Patrick Meenan

Welcome!

This is my personal blog and mostly contains my random thoughts about web performance, development, browsers or whatever else I might be thinking about at the time.

Latest Posts

news-tldr.com - My take on an AI-curated news feed

I’m a news junkie (well, really an information junkie). Usually I consume it through a combination of listening to WTOP news radio when commuting and a Feedly curated feed that I scan from time to time (usually once a day in the evenings). RIP Google Reader!

The main problems I have with my current methods are that there is a lot of cruft and it is more time consuming than it should be:

  • The Feedly aggregated RSS feeds have a LOT of duplicated content from multiple sources.
  • There is a mix of “sponsored” posts, paid placements and actual stories in news sources these days.
  • There are a LOT of ads on the radio broadcast (I’d say 50/50 content vs ads).
  • There is a LOT of bias (in both directions) in the reporting, depending on the source.
  • There is a lot of low-information clickbait making scanning hard.

Most of that seemed solvable by AI these days so I went ahead and built it. news-tldr.com is my take on a modern editorial news feed that is focused on “just the facts” and made for easy skimming with minimal duplication and some level of ordering from “top news” to less-impactful articles.

Screenshot of news-tldr.com's home page
The news-tldr.com homepage.

The site itself is using the feeds I curated (intentionally using sources domestic and international with left and right-wing bias included). It’s mostly intended for personal consumption but feel free to use it if you find it useful. It publishes hourly and has a rolling set of articles from the last 72 hours (though it is set up to default to only showing new content you haven’t seen yet).

The code is all on GitHub with an Apache 2.0 license so feel free to fork it or use any part of it for your own purposes as well.

The news-tldr pipeline factory floor
The article pipeline from ingestion, through data extraction and editorial to final site.

How it works

The workflow is basically a pipeline that takes all of the articles it can get its hands on as input and spits out a curated “here’s what you need to know” set of articles. It largely mirrors what I’d have to do in my head manually off of the raw feeds before.

Article ingestion

The first step of the pipeline is to get the raw data and extract the key information from each article:

  • Fetches all of the new articles from the configured RSS feeds.
  • Uses Gemini 3.5 flash-lite to extract the contents and key points from each article.
  • Ranks the “impact” of each article, flagging sponsored or paid content (in the same flash-lite extraction pass).

From this we end up with a clean set of summarized articles with keywords and facts extracted (and original text and source link maintained).

Fetching, text extraction, and Flash-Lite digest scoring
Extracting clean article text, stripping hype and bluster, and filtering out sponsored and low-impact content.

Article grouping

From there, the summarized articles are grouped and de-duplicated against each other so individual multi-sourced “events” can be reported as a single story:

  • Articles are grouped based on extracted keywords.
  • Gemini 3.5 flash-lite is given batches of articles to group, both new and existing articles as well as a list of existing groups.
  • Questionable “borderline” cases are handed to Gemini 3.7 flash for decision.

At this point we now have a bunch of “stories”, a lot of which are multi-sourced with several supporting articles.

Clustering and deduplicating related coverage
Partitioning coverage into sliding windows and category channels, clustering related articles, and merging duplicates into unified event dossiers.

Editorial

The final stage in the pipeline is to take the hundreds of individual stories and curate an editorial page with related stories grouped together and more-important stories being displayed first.

To keep the models from hallucinating and avoid blowing up token costs by feeding full article text to the larger models, a fast Flash-Lite pass first extracts an “evidence ledger” of key claims backed by exact verbatim quotes from the articles (which code verifies actually exist in the source text). Gemini 3.7 flash then drafts the neutral headline and two-bullet briefing strictly from that ledger, followed by an independent verification pass that cross-checks every claim against the quotes before stamping it approved. It also handles the final homepage curation to pick the top news and group related topics.

Extracting evidence, drafting, and independent verification
Building a passage-backed evidence ledger, drafting neutral briefings strictly from evidence, and independent verification.

Architecture and tech stack

Just to be clear, in case there was any doubt, AI wrote all of the actual code (and generated the images for this article). I steered the architecture, drove the work and reviewed the results but I don’t think I wrote a single line of the actual code. Models from pretty much all of the frontier labs were involved over the last few weeks in building it:

  • Google (in Antigravity): Gemini 3.1 Pro, 3.7 Flash and 3.8 Flash (all on High)
  • OpenAI (in Codex): Sol 5.6 (high and extra high), Astra (medium and high)
  • Anthropic (in Claude Code): Opus 5 (high), Fable 5 (high), Fable 5.1 (medium and high)

They served both in building the pipeline/writing the code as well as judging the Gemini model’s success at each stage of the pipeline, refining the API calls and prompts to make sure the data extraction, grouping and editorial passes were good.

Gemini API and models

One of the less-appreciated features of the Google AI Ultra subscriptions is that you get a $100 monthly Google cloud credit that can be used for, among other things, the Gemini API. That’s why pretty much everything in the pipeline uses some flavor of Gemini. I’m still not exactly sure what byzantine labyrinth I went through actually worked, but I think it was activating an API key through AI Studio that is linked to my “personal” billing account (which I may have had to accept the developer program to link - I can’t remember).

Since this is a batch pipeline, I am also using the “flex” priority when calling the API which cuts the costs in half and still provides responses in a few seconds. This is why it is using 3.7 flash instead of 3.8 though, because 3.8 was erroring with the flex tier (probably capacity constraint).

As far as I can tell, the actual costs are coming in way under the promotional credit, but that’s not saying much because I really have no idea from the current billing dashboard. Worst-case it is costing me $0.20 per day but I really have no idea what to make of the actual reports (why are all cloud products so bad?).

API daily spend
API daily spend.

I’d sure be a lot more comfortable if I knew what “savings” were from and where my cloud credits factor into this.

If I was actually doing this for real, I’d be REALLY inclined to buy a DGX spark or two and run models that I control (though you probably don’t want Chinese models doing the editorial on news content).

Tech stack

The actual tech stack itself is as boring as it gets:

  • Cron scheduling.
  • Python for running the actual pipeline (fetch, AI API calls, producing the static HTML and JSON).
  • Raw articles stored in flat-files on disk.
  • SQLite datastore for the extracted and processed article details and editorial decisions.
  • Static HTML single-page app, served with 10-minute cache through Cloudflare.
  • Vanilla JavaScript for in-page interactions, read article tracking, etc.

Cross-browser linking

I use browsers on a lot of devices and REALLY wanted to be able to synchronize the read articles across them so I could pick up any device and just see unread news.

The easy answer would be to allow for login and track the read state per-user but that’s more complicated than I really wanted (and there’s no reason for the site to have the actual email address of the visitor). I’ve set up OAuth login before using the well-known providers and it’s a pain to get the site registered and working and to do the back-end validation.

What I REALLY wanted was some form of profile-based storage that the site could request, user could approve and it would sync with your browser account across devices (like history, extensions, etc). That could have provided a zero-infrastructure mechanism for sharing without the server having to have any knowledge. Sadly, no such thing exists (outside of extensions).

What I ended up building was a simple token/URL-based linking system where any browser can request an unguessable “token” from the server which gives them a URL that can be opened on any device and that browser starts syncing the read list through the server using the shared token. I then just email the URL to myself and open it on any device that I want to use the shared state (where it is stored in localStorage).

There’s an icon on the top-right of the page that shows the current state and allows for creating tokens or unlinking from an existing group.

What’s next

The time I spend commuting is still pretty much wasted listening to local news radio and all of the ads. It would be fairly easy to have Gemini generate a podcast script and use one of the TTS models to create a convincing podcast of the most recent 12 hours of news (drive in and drive home). I already sketched out what that would add and it plugs in to the end of the pipeline really cleanly but I’m not sure about the costs which is the main thing holding me back.

Robots discussing the news in a podcast studio
AI co-hosts discussing the latest news-tldr.com briefing in a podcast studio.

I’d probably set it up to produce hourly so time zones won’t matter (you get the last 12 hours whenever you listen) and it would be structured so it is front-loaded with the key information in the first 5 minutes and gradually gets into more depth and stories as it goes on so that it could support short and long commutes.

I set up a JSON endpoint at /api/brief.json that is published hourly when the page is updated that has all of the stories and editorial details for a rolling 12-hour window. I may play with some tooling manually to see how well it works in something like NotebookLM or AI Studio.

What I’d really love to see is for someone to actually start going in this direction for a REAL news feed from a real provider rather than something I cobbled together for personal use.

CVEs, LLMs and WebMCP - Oh My!

Like most of my recent projects, it started out with an itch I wanted to scratch so, instead of just answering the question I wanted answered, I WAY over-engineered a solution and used AI to build something instead.

In this case, the question I had was: “Are we seeing a shift in CVEs after the initial post-Mythos spike to fewer critical CVEs as the backlog of undiscovered issues is cleared out?” On the receiving end in Chrome it kind of felt like we had crested a wave but I wasn’t sure if that was just my little pocket of the world or applied more broadly.

I started by looking for a tool that I could just query and see what the history looked like, and maybe my search-fu is just weak, but all of the tools I could find pretty much sucked. And, to top it off, the CVE Dataset is a GitHub repository of JSON files - not exactly conducive to running a quick analysis.

So I built CVE Explorer.

A screen shot of a website showing a stacked bar chart of CVEs by severity over time, increasing sharply over the last few months.
CVEs by severity over the last two years.

The Original Plan

Originally I had planned to build an in-browser visualization tool using a local sqlite representation of the dataset but, instead of making the user manually filter and select from a representation that I picked, give them a chat bot that had access to the sqlite dataset as well as graphing tools to represent the results. I ended up with a bit of a hybrid, allowing for some quick manual filtering but the core of the idea landed. The devil is in the details though.

Sqlite dataset

The original plan was 100% in-browser using Origin Private File System (OPFS), wasm and sqlite and that is still supported with a full sync and local analysis but the friction seemed a bit high if someone had a casual question they wanted answered quickly so I caved and also added a server-queryable sqlite dataset.

At this point I’ve used WASM, OPFS and sqlite in a few of the hobby projects and it works incredibly well, running in worker threads, off of the main UI thread and with good performance. Probably not something I’d want to target at low-end mobile devices but for my developer desktop target demographic, it’s fine.

Charting engine

Originally I had expected to use an off-the-shelf library or d3.js since those have been my historical methods for dropping charting support into a tool quickly, but the agents convinced me that it would be just as easy to build something custom with svg and I could make it do exactly what I wanted it to do without being bound by the library.

I added support for stacked and grouped bar charts, line and area charts and data tables figuring that should cover every way someone would want to visualize the underlying data. It was also important that there be a “copy as png” button to make it easy to share the resulting visual.

It’s not particularly feature-rich but it does exactly what I needed it to do and let me offer things like interactive hovering, hiding series by clicking the labels, shading “partial” data columns (like the current month), etc.

In-app AI chat

This is where there was the most churn (probably not surprising). There are a bunch of ways of integrating a chatbot (with tool calling) that I considered:

  • In-browser model using WebGPU and WASM.
  • Chrome’s Prompt API.
  • Leverage the Google cloud credits key that comes with my Google One subscription.
  • Run a small model on my local server.
  • Support user-provided API keys for common services (open router, etc).
  • Integrate with “Ask Gemini” in Chrome.
  • Support chat extensions from the various AI providers.

The initial plan was to start with an in-browser model using WebGPU and WASM since I’ve used that in a few projects already and it works great - once you get past the initial 4+ GB download of the model. And THAT friction for answering a quick question is what sent me to running my own local model on my server.

A screen shot of a chat box showing Chrome CVEs spiking and receding.
Asking about Chrome's CVE trends over time.

I wanted to keep it simple so I’m just running ollama on a machine with a GPU that used to be my son’s gaming PC before I turned it into a linux dev machine (so, clearly not production infrastructure but it works great).

The big question was “what model?”. I had originally planned to use one of the Gemma 4 variants but it wasn’t going great (the tool calling was hit-or-miss) so I had one of the AIs that was helping build the thing test a bunch of models that would fit on my 16 GB card to see which reliably produced accurate SQL and reliable tool calling and we settled on Qwen3:8b with a 32K context window. It’s a bit long-in-the-tooth at this point but I didn’t need anything with fancy knowledge or code generation, I needed reliable SQL and it fit the bill.

It works great for asking it arbitrary questions about the data. It will use the charting engine to generate graphs or answer questions directly and can run comparative analysis and do way more than just drive the charting engine (which it can also do).

That said, it’s a toy compared to the frontier models and can’t pull in recent information from search and other data sources.

”Ask Gemini” to the rescue?

Since I have a family Google One subscription, leveraging the built-in Gemini integration in Chrome seemed like an easy way to use frontier-level models with the same dataset but also pull in external sources and broader knowledge.

I added JavaScript APIs for the dataset, provided a sandboxed iframe for running arbitrary code on query results, added WebMCP wrappers for all of the API surfaces (to cover my bases) and added hidden documentation in the HTML describing the dataset schema and API surfaces.

… and then I realized that the “Ask Gemini” integration is read-only and it can only scrape the contents of the page and not be an active participant in running queries and scripts (at least for now).

WebMCP and AI chat extensions

Luckily, the ChatGPT and Claude extensions have no such qualms about reaching into the page and monkeying about with scripts. Not surprising since they are also developer-focused and can drive the dev tools API but they will happily drive the page-provided APIs for querying data and integrate it with any other sources they decide to use. As it turns out, keeping them on-topic and using the actual dataset is harder than I expected and they like to just answer based on search data rather than actually looking so you have to be careful and steer them towards using the CVE data in the dataset to inform their answers. They can also take a bit of steering to force them to actually USE the APIs rather than automating the UI.

They’ll also happily use the built-in charting engine and PNG generation to extract images of charts to go with their answers.

Where WebMCP falls short

It’s clear that WebMCP was primarily designed for giving AI access to interact with and drive the page but more as an input and less as another UI surface that the application is aware of. That works great for headless bots like Hermes, Pi, Gemini Spark or one of the other “assistant” harnesses and it is good for giving user-initiated tooling access to the page but it’s not a particularly good API for building an “AI-first” application that actually integrates with user-provided tooling.

If the user happens to have an AI subscription and happens to know that my page offers WebMCP support and happens to open their extension, THEN they can actually use the frontier-quality chat integration that they are already paying for. But from the application side there is no clean way to signal to the user that it’s available or to better integrate the internal chat experience with the extension-provided experience.

I filed a GitHub issue with the WebMCP group to at least raise the point in case it wasn’t already being considered so we’ll see where it goes.

Optimally, I’d love to see something where browser extensions can register “LLM Chat” and “WebMCP” support through their manifests, pages can advertise support either through markup or WebMCP APIs and the browser can manage nudging the user, opening the chat if one is available and offering a picker if multiple options are available.

Did Chrome’s CVE rate crest?

… and is it representative of a broader trend in the industry?

Maybe and No, respectively:

Stacked bar chart of Chrome's CVE publications by month for the last year, spiking in June and coming down in July.
Chrome CVE trend by month for the last year.

Using Opus 5 in the Claude extension and the JavaScript-provided APIs:

Chrome’s CVE surge peaked in early June 2026 and has been receding since. Measured in aligned 30-day windows, Google Chrome went from 74 CVEs (late Feb–Mar) to 249 (late Apr–May) to a peak of 731 (May 26–Jun 25), then fell to 502 and 438 in the two windows that followed. For scale: Chrome’s total annual CVE count ran between 195 and 357 every year from 2018 through 2025. In 2026 it has already published 2,140 — its peak month alone is roughly five times its entire 2025 output.

The rest of the industry shows no such crest. Total published CVEs rose in every one of those same six windows, from 6,115 to 11,455, an all-time high. The corpus recorded 48,154 CVEs in all of 2025 and has already passed 54,999 by late August 2026, annualizing to roughly 85,000. Of the 27 vendors with meaningful recent volume, nine are peaking in the most recent window and four peaked one window back; only Google, Adobe, and one small WordPress theme vendor show Chrome’s shape of a peak followed by two consecutive declines — and Adobe’s dip is far shallower. Chrome’s share of all published CVEs went 1.2% → 9.1% → 3.8% in six months while everything around it climbed.

One caveat: Chrome publishes in monthly bulk drops, and it is the drop sizes that crested — 59, then 125/78/151, then 427, 382, 370. The escalation stopped in early June and the last three drops are flat. Read the current decline as a plateau easing off rather than a collapse.

(Data: the CVE List (cvelistV5) as of 2026-08-24, via cve.meenan.dev.)

It’s probably too early to call it a trend, but yeah, that spike felt very real!

Why all the effort?

But Why? Meme.
But Why?

First, and most importantly, I enjoy building things and AI has reduced the effort significantly so I can be more ambitious with exploring even if I’m the only one who uses the result.

Secondly, I can use these as a way to experiment with the platform and see where the rough edges are (and hopefully help improve them). I STRONGLY believe that you need to use the technologies you are building to get a feel for how they should work (implementations, not specs). The web platform is still the best platform for building most things on with cross-platform support, built-in sharing and sandbox safety so I like to see if there are any limiting factors that CAN’T be built there (native socket support is the main one I’ve run into - but rarely).

Testing Compression Dictionary Deployments

Robin Marx has been asking me for a Compression Dictionary Transport (RFC 9842) test tool for a long time (since well before AI was actually useful for this kind of work). Then Sergey Chernyshev brought it up again recently. So, I finally stopped procrastinating and spent a few hours this weekend working with AI to build out a proper test tool called compdict.

It turns out that using AI makes building test suites for web specs and protocols MUCH easier than doing it by hand, and it is something we should start leaning on a lot more.

A developer surrounded by computer monitors displaying terminal windows and browser error messages.
Debugging Compression Dictionary Transport issues across multiple monitors.

Bouncing Ideas and Spec Analysis

I started by bouncing ideas back and forth between Opus 5 and GPT Sol 5.6 (still waiting for Gemini 3.5 Pro to drop). I pointed both models at the RFC and the fetch spec pull request to work through what a useful test tool would look like and how it ought to work.

We settled on a Node CLI tool runnable directly via npx compdict that also exposes a JavaScript API surface in case anyone wants to integrate it with a CI pipeline.

Beyond validation, I also wanted it to handle offline encoding so you can generate dcb and dcz delta-compressed assets directly in your build pipeline:

npx compdict encode dist/app.v2.js \
  --dictionary dist/app.v1.js \
  --encoding dcb \
  --hash

Using --hash appends the dictionary SHA-256 hash directly to the output filename (matching the Structured Field Byte Sequence in Available-Dictionary), making the output assets directly addressable from static storage or a CDN edge without needing on-the-fly compression.

To keep installation painless and avoid native C++ build toolchain headaches in locked-down CI images, we chose to bundle both Brotli and Zstandard via WebAssembly. That gives compdict zero runtime native dependencies and known-good codec implementations. When I had tried getting Brotli and Zstandard WASM builds working by hand in the past, it took me a day or two of pain for each one. The AI knocked out the bundled WASM setup without breaking a sweat.

I gave the models a few edge cases I was already aware of, and then had them look through the specs to identify a bunch more that should be tested. Between the two of them, they identified edge cases around content-encoding negotiation, header casing, URL pattern matching, and HTTP protocol parity that I almost certainly would have missed. One model generated the initial plan and the other model reviewed it.

Putting AI to Work Overnight

Once I was happy with the plan, I asked GPT to orchestrate a bunch of subagents to build it out overnight so it would be ready for me to iterate on in the morning. I picked GPT for the implementation because it tends to be better at following directions, sticking to a plan, and thinking through edge cases (at least for me).

In the morning, I ran a few more rounds of having Opus code review GPT’s work, had GPT fix the issues that Opus came up with, and then started running live tests against my dev test pages to see how it works and going back to GPT with change requests (once I have once agent implement, I usually have the same one do the revisions and changes - though often in new conversations).

Finding Bugs on My Own Site

Funny enough, my own test site was littered with issues (well, my wife’s road trip blog but what she doesn’t know…):

  • Negotiation bypass: It would serve dcz (Zstandard dictionary) responses as long as Available-Dictionary was announced, even if the client didn’t advertise dcz in Accept-Encoding.
  • Caching lifetime: The static assets are all cache-control: no-cache, making dictionaries useless for delta updates (whoops).

It did help refine the checks though. All of these tests are something I would have done if I had coded a test tool myself, but it is WAY more thorough than I ever would have been. And more importantly, it actually created the tool while I’ve been procrastinating doing it myself for 2 years (does that mean procrastination worked out again? - lazy programmers FTW!).

Giving It a Spin

If you are serving or testing dictionary-compressed responses on your origin or CDN, you can run compdict directly against any URL:

npx compdict validate https://example.com/static/app.v1.js

It validates header correctness, Vary: available-dictionary behavior, URL pattern scope, and HTTP/1.1 vs HTTP/2 protocol parity, showing exact byte savings for dcb and dcz encodings.

The package is live on npm at compdict and the source code is on GitHub. Please file issues for anything you see that is broken or missing from the spec checks, and contributions are always welcome!

View All Posts in Archive
Enlarged view