Conversation
Every unit so far assumed a runtime we control the shape of -- a browser running ad.js, or a terminal printing text. A feed is neither. The document is built by somebody else's static site generator, spliced into a <channel> whose root element we did not write, and then kept for weeks by software that remembers what it has already shown. So `feed_item` is a new format rather than a new renderer for an old one, and three constraints drove it. **No namespaces.** The RSS fragment uses core RSS 2.0 elements only and the Atom fragment core Atom only. A <dc:creator> whose prefix the publisher never declared does not make our item ugly, it makes their whole feed not well-formed -- every reader drops the entire document rather than the one element. That rules out the advertiser name in dc: and the logo in Media RSS, both nicer, neither worth an outage for every subscriber of every publisher carrying the unit. A test asserts no prefixed element can appear. **No CSS.** Readers strip <style> blocks and most strip style= too, so the hierarchy is carried by semantic elements and the body reads correctly with every attribute removed. Links are rel="sponsored nofollow noopener": a paid link copied verbatim into every aggregator that mirrors the feed is the exact shape of a link scheme. **A periodic identity.** guid is the whole of a reader's memory. Mint one per fetch and the ad resurfaces as unread on every rebuild, which is how a feed gets unsubscribed from; freeze it forever and the advertiser reaches each subscriber once. So it rotates -- daily by default, keyed by *slot* rather than campaign, because the campaign changes on every fill and would defeat the mechanism within the hour. weekly/fill/static are there for feeds the default is wrong for. Seven wire shapes off one creative (rss, atom, json, html, markdown, text, fields) and three body styles (text, card, terminal). `fields` is the integration contract: a consumer that already builds all three formats should render the ad through its own renderers, so that one piece of software decides how a title gets escaped inside one document. Two things this would have got wrong without care. Feed builders identify as HTTP libraries, which the generic tracker calls a bot, and bots get the unmetered house ad -- so a feed slot could never have earned a cent. feedDeviceType fixes that the way terminalDeviceType did, and checks the reader patterns *before* the crawler ones because Feedly's fetcher advertises itself as "like FeedFetcher-Google". And feed fills take the short /a/<code> click URL: as=text, as=markdown and style=terminal all print it as literal text, where the 70-character form does not fit the box. Also: snippets are data now. The banners needed two branches in a component because the browser was the runtime; the feed unit has to be installed in whatever language builds the publisher's feed, so lib/ads/snippets carries 36 recipes across Node, Next, Express, Hono, Eleventy, Hugo, Jekyll, Astro, WordPress, PHP, Python, Django, Ruby, Go and Cloudflare Workers. Every one of them fails open, because a feed build that throws when an ad server is slow takes the publisher's whole deploy with it. Metering is unchanged and worth being explicit about: the impression is recorded at fetch time, so one build produces one impression for a document thousands then read. Feed impressions undercount reach by design; clicks are exact. The migration follows terminal_ascii exactly -- widen the CHECK, add the format to slot inventory, backfill a creative per campaign from the copy it already has. Apply by hand via psql over the pooler; prod history diverged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
ThreatCrush Security Scan35 finding(s) HIGH/CRITICAL: 3 | MEDIUM: 23 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
copyToCreatives maps over every format, so every new campaign now gets a feed_item creative -- and AdPreview had no branch for it, so all three advertiser screens (new, detail, edit) fell through to the banner renderer and drew it as a 600x120 box. That is not what anybody receives. The preview is deliberately not painted in the advertiser's brand colours. The served body carries no CSS at all, because feed readers strip it, so its appearance comes entirely from the subscriber's own stylesheet -- and showing brand colours here would promise a look we have no way to deliver. What it shows instead is the structure that does survive: the disclosure, the headline as a link, the body, the call to action, the attribution, on the plain serif-on-white a reader actually renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
feed_item— an ad served as a syndication item, for splicing into a feed a publisher generates.What
GET /api/ads/feed?slot=<id>returns one of seven shapes off a single creative:as=rss<item>, ready to paste inside a<channel>atom<entry>jsonhtml/markdown/textfieldsPlus
style=text|card|terminal,guid=daily|weekly|fill|static,n=1..5,label=,cols=,src=.Three constraints that shaped it
No namespaces. A fragment is pasted inside a
<channel>whose root element we did not write. A<dc:creator>whose prefix the publisher never declared does not make our item ugly — it makes their whole feed not well-formed, and every reader drops the entire document. Core RSS 2.0 and core Atom only; a test asserts no prefixed element can appear.No CSS. Readers strip
<style>and most stripstyle=. Hierarchy is carried by semantic elements, so the body reads correctly with every attribute removed. Links arerel="sponsored nofollow noopener"— a paid link mirrored into every aggregator is the exact shape of a link scheme.A periodic identity.
guidis the whole of a reader's memory. Per-fetch and the ad resurfaces as unread on every rebuild; frozen and the advertiser reaches each subscriber once. Default is daily, keyed by slot rather than campaign — the campaign changes on every fill and would defeat the mechanism within the hour.Two traps avoided
feedDeviceTypefixes this the wayterminalDeviceTypedid, checking reader patterns before crawler ones (Feedly’s fetcher advertises itself as "like FeedFetcher-Google")./a/<code>click URL:as=text,as=markdownandstyle=terminalprint it as literal text, where the 70-char form does not fit the box.Snippets are data now
lib/ads/snippets.tscarries 36 recipes — Node, Next, Express, Hono, Eleventy, Hugo, Jekyll, Astro, WordPress, PHP, Python, Django, Ruby, Go, Cloudflare Workers, and the existing web/terminal ones. Every feed recipe fails open: a feed build that throws when an ad server is slow takes the publisher's whole deploy with it.Metering
Unchanged, and worth stating: the impression is recorded at fetch time, so one build produces one impression for a document thousands then read. Feed impressions undercount reach by design. Clicks are exact.
Deploying
supabase/migrations/20260818120000_ad_feed_item.sqlfollows theterminal_asciipattern (widen the CHECK, add to slot inventory, backfill a creative per campaign). Apply by hand via psql over the pooler — prod migration history diverged, do notsupabase db push.Verification
tsc --noEmitclean,next buildclean with/api/ads/feedregistered.fast-xml-parseradded as a devDependency: the core risk is emitting XML into someone else’s document, so well-formedness is asserted with a real validator rather than a regex.Consumed by profullstack/rssamplifier.com#feed-ads.
🤖 Generated with Claude Code