Part of degoogling my life meant dropping the YouTube app. The only thing I actually used it for was the subscription feed: what the 10 to 15 channels I follow have posted, newest first. No comments, no recommendations, no Shorts. So I rebuilt that one screen and nothing else, on the same edge layer as the rest of this site.
Just the feed, nothing else
It’s one chronological list: the newest videos across every channel I follow, merged and sorted newest first. You add channels from the page by URL or @handle and they stick around.
Everything YouTube stacks on top of the subscription feed is something I ignore or actively avoid, so building only the screen I use was less work than configuring the app to leave me alone.
How it works without an API key
Every channel publishes a public RSS feed at feeds/videos.xml with its recent uploads as Atom XML, and the Worker reads that directly, parsed with fast-xml-parser. No quota, no Google project, no token to rotate.
People add channels by handle or vanity URL, and those change, so the Worker scrapes the channel page once to resolve whatever you typed to a stable channel ID, then uses that ID from then on. The input goes through an SSRF guard first, because “fetch whatever URL the user pasted” is how you turn your own Worker into someone’s proxy into your network.
Filtering out YouTube Shorts
Shorts were the main thing I wanted gone, and one check doesn’t catch them reliably. So there are two: a title heuristic for anything tagged #shorts, and a per-video redirect probe, since Shorts and full videos resolve through different URLs. A video has to clear both to show up.
Playback without the tracking
Click a thumbnail and the video plays inline through YouTube’s nocookie IFrame player. The thumbnails are static images and the embed only loads on click, so no tracking cookies are set until you press play. It isn’t perfect privacy, nocookie still writes a localStorage ID and sends your IP to Google once the player loads, but it’s a long way better than the app. On iOS it autostarts muted with a “Tap for sound” overlay, because Apple blocks one-tap sound on the web and a muted-but-playing video is the least-bad workaround. Videos that block embedding get an “open on YouTube” fallback instead of a dead frame.
Picture-in-picture came for free
A side effect I didn’t plan for. Because playback is a plain web video element and not the YouTube app, the system controls come with it. On iOS that means picture-in-picture and background audio for nothing: the floating window follows me across apps, and sound keeps going when the screen locks.
Fast and hard to break
Feeds fetch in parallel with Promise.allSettled and per-channel timeouts, so one slow or dead channel resolves to nothing instead of hanging the page. The merged result is edge-cached with the Cache API for 15 minutes, and the channels you add live in Cloudflare KV. The page is server-rendered to an HTML string and deployed with wrangler.