Skip to content

Make the feed ad worth opening: artwork, a brand line, a quieter title - #201

Merged
ralyodio merged 1 commit into
masterfrom
feed-ads-polish
Aug 18, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feed-ads-polish

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Follow-up to #200, reported off rssamplifier.com/topics/rust.rss: the ads read as spam and nobody would open them.

Three changes, none of which touch what the advertiser wrote.

The title no longer leads with the label

[Sponsored] Ship faster → Ship faster (Sponsored)

A leading bracket is the first thing the eye meets in a list of headlines and reads as a spam subject-line prefix, so the item is skipped before the offer is ever read. Trailing, the headline gets to say what it is and the label still travels everywhere the title travels — plus <category> carries it machine-readably. Nothing is hidden: same word, same field.

The card carries the artwork

A feed item sits between real blog posts that each have a title, a picture and a few paragraphs. A bare line of text next to them does not read as restrained — it reads as broken. The card now leads with the hero image, then an <h3> headline, the body, the CTA.

width is set and height deliberately is not: readers scale to their own column, and a fixed height distorts every image that is not exactly the ratio we guessed.

A brand line names who is paying

The logo where there is one, and always the destination host. "Sponsored" tells a reader an ad is an ad; the domain tells them whose it is, which is what they actually decide on.

The host cannot come from our click URL — that is always crawlproof.com — so destinationHost reads it off the advertiser's own logo/image, and returns "" rather than guessing when the only candidate is our own storage.

Migration

20260818160000_ad_feed_item_artwork.sql undoes a decision from the format's first migration, which dropped image_url on the reasoning that a feed item has no artwork. That was right about the one-line text style and wrong about the format.

Restores it from each campaign's medium rectangle — the creative the hero image was resolved for. 96 of 106 campaigns; the other 10 render a card with no picture, which the renderer treats as ordinary rather than assuming an image. Already applied to prod ({"success": true}, verified 0 → 96).

Not done on purpose

No copy is invented. Bodies average 76 characters because that is what the advertisers wrote — an ad describing a product in words its owner never wrote is a fabricated claim however good it looks. What changed is the layout around it.

Verification

1507 tests pass (6 new), tsc --noEmit clean, next build clean.

Paired with profullstack/rssamplifier.com#104, which fixes the ads sorting to the top of the feed.

🤖 Generated with Claude Code

Reported off rssamplifier.com/topics/rust.rss: the ads read as spam and nobody
would open them. Three changes, none of which touch what the advertiser wrote.

**The title no longer leads with the label.** "[Sponsored] Ship faster" becomes
"Ship faster (Sponsored)". A leading bracket is the first thing the eye meets
in a list of headlines and it reads as a spam subject-line prefix, so the item
is skipped before the offer is ever read. Trailing, the headline gets to say
what it is and the label still travels everywhere the title travels -- and
<category> carries it machine-readably besides. Nothing is hidden; the same
word is in the same field.

**The card carries the artwork.** A feed item sits between real blog posts that
each have a title, a picture and a few paragraphs. A bare line of text next to
them does not read as restrained, it reads as broken, and gets scrolled past.
The card now leads with the advertiser's hero image, then an <h3> headline, the
body, and the call to action. width is set and height deliberately is not:
readers scale to their own column, and a fixed height distorts every image that
is not exactly the ratio we guessed.

**A brand line names who is paying.** The logo where there is one, and always
the destination host. "Sponsored" tells a reader an ad is an ad; the domain
tells them whose it is, which is what they actually decide on. The host cannot
come from our click URL -- that is always crawlproof.com -- so destinationHost
reads it off the advertiser's own logo/image and returns "" rather than
guessing when the only candidate is our own storage.

The companion migration undoes a decision from the format's first migration,
which dropped image_url on the reasoning that a feed item has no artwork. That
was right about the one-line `text` style and wrong about the format. Restores
it from each campaign's medium rectangle, which is the creative the hero image
was resolved for: 96 of 106 campaigns. The other 10 render a card with no
picture, which the renderer treats as ordinary rather than assuming an image.

No copy is invented. Bodies average 76 characters because that is what the
advertisers wrote; an ad describing a product in words its owner never wrote is
a fabricated claim however good it looks. What changed is the layout around it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

35 finding(s)

HIGH/CRITICAL: 3 | MEDIUM: 23 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:47
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:98
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit f6fd26b into master Aug 18, 2026
8 checks passed
@ralyodio
ralyodio deleted the feed-ads-polish branch August 18, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant