# Rss Feed Fix

> Fix a broken RSS feed generator by downloading the live HTML, comparing it against the current CSS selectors in the generator, and updating any selectors that no longer match. Use when a feed is EMPTY or has stopped updating, after a validate_feeds.py failure, or when asked to "fix feed", "feed is broken", or "selectors broke".

- Skill: `olshansk/rss-feed-fix` (Agent Skill)
- Install (CLI): `npx skillmds@latest add olshansk/rss-feed-fix`
- Raw SKILL.md: https://api.skillmd.com/api/skills/olshansk/rss-feed-fix/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: olshansk (https://skillmd.com/u/olshansk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/olshansk/rss-feed-fix

---


# RSS Feed Fix

You are the **RSS Feed Fix Agent**. A feed generator is broken — its CSS selectors no longer match the live site. Your job is to download the current HTML, identify what changed, update the selectors, verify the fix, and add a safety guard if one is missing.

## Inputs

The user should provide:
- The feed name (e.g. `weaviate`, `cursor`) OR the generator filename (e.g. `weaviate_blog.py`)

If neither is provided, check `feed_generators/validate_feeds.py` output or `feeds/feed_*.xml` to identify which feed is EMPTY.

## Workflow

### Step 1: Identify the generator and blog URL

1. Map the feed name to its generator:
   - Feed name → `feed_generators/{name}_blog.py`
   - If ambiguous, check `feeds.yaml` for the `script:` field
2. Read the generator file in full.
3. Note: `BLOG_URL`, `FEED_NAME`, and every CSS selector used in `parse_*` functions.

### Step 2: Fetch the live HTML

Use `curl` to download the raw HTML (not the WebFetch tool — it converts to markdown and loses class names):

```bash
curl -s "{BLOG_URL}" -o /tmp/feed_fix_live.html
```

If the site blocks `curl` (empty body or 403), try with a browser-like User-Agent:

```bash
curl -s -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" "{BLOG_URL}" -o /tmp/feed_fix_live.html
```

If the site is JavaScript-rendered (SPA), note that and advise the user to provide a saved HTML file. Do not attempt to spin up Selenium during a fix — that requires a separate debugging session.

### Step 3: Diagnose the broken selectors

For each selector used in the generator's `parse_*` functions, check whether it still matches:

```bash
python3 -c "
import sys
sys.path.insert(0, 'feed_generators')
from bs4 import BeautifulSoup

with open('/tmp/feed_fix_live.html') as f:
    soup = BeautifulSoup(f.read(), 'html.parser')

# Test each selector from the generator
selectors = [
    'article.margin-bottom--xl',   # replace with actual selectors
    'a[itemprop=\"url\"]',
    'meta[itemprop=\"description\"]',
    'time[datetime]',
]
for sel in selectors:
    results = soup.select(sel)
    print(f'{len(results):4d} matches  {sel}')
"
```

Selectors that return 0 matches are broken. For each broken selector, find the replacement:

```bash
python3 -c "
import sys, re
with open('/tmp/feed_fix_live.html') as f:
    html = f.read()

from collections import Counter
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')

# Show top repeated tag+class combos — likely structural elements
combos = Counter()
for tag in soup.find_all(True):
    if tag.get('class'):
        combos[(tag.name, ' '.join(tag['class']))] += 1
print('TOP REPEATED ELEMENTS:')
for (tag, cls), count in sorted(combos.items(), key=lambda x: -x[1])[:30]:
    print(f'{count:4d}  <{tag} class=\"{cls}\">')
"
```

Then inspect a single representative element to confirm the new structure:

```bash
python3 -c "
from bs4 import BeautifulSoup
with open('/tmp/feed_fix_live.html') as f:
    soup = BeautifulSoup(f.read(), 'html.parser')
# Replace with the new candidate wrapper selector
el = soup.select_one('article')  # or whatever looks right
if el:
    print(str(el)[:3000])
"
```

### Step 4: Update the generator

For each broken selector, make the minimal targeted edit using Edit tool. Preserve all logic — only change the selector strings and the attribute/text access that follows them.

Common selector patterns to look for in replacements:

| What to find | Typical old pattern | Look for new pattern |
|---|---|---|
| Article URL | `a[itemprop="url"]` | `a.{css-class-on-link}`, `header a`, `h2 a` |
| Description | `meta[itemprop="description"]` | `p.{description-class}`, `.summary`, `.excerpt` |
| Date | `time[datetime]` | usually still `time[datetime]` — rarely changes |
| Title | `h2`, `h3` | usually stable — check if now inside a different wrapper |
| Article wrapper | `article.{class}` | check for new class on `article` or `div` wrapper |

### Step 5: Add the empty-posts guard (if missing)

Check whether `main()` has a guard that prevents saving an empty feed:

```python
if not posts:
    logger.warning("No posts fetched — skipping feed update to avoid overwriting with empty feed")
    return False
```

This guard must appear **after** the posts list is built (after merge with cache for incremental generators) and **before** `save_cache()` and `save_rss_feed()`. If missing, add it.

### Step 6: Verify the fix

Test the updated parser directly against the downloaded HTML:

```bash
python3 -c "
import sys
sys.path.insert(0, 'feed_generators')
from {module_name} import parse_posts  # or parse_articles, parse_blog_html, etc.

with open('/tmp/feed_fix_live.html') as f:
    html = f.read()

posts = parse_posts(html)
# Some generators return (posts, has_next) — adjust accordingly
if isinstance(posts, tuple):
    posts, has_next = posts
    print(f'has_next={has_next}')

print(f'Found {len(posts)} posts')
for p in posts[:3]:
    print(f'  {p[\"title\"][:60]}')
    print(f'  {p[\"link\"]}')
    print(f'  {p.get(\"date\")}')
    print()
"
```

A successful fix shows 5+ posts. If still 0, revisit Step 3 — check the HTML more carefully; the site may require JavaScript rendering.

Then run the full generator to write the feed file:

```bash
uv run feed_generators/{name}_blog.py
```

Then validate:

```bash
uv run feed_generators/validate_feeds.py
```

The target feed should no longer appear as EMPTY.

### Step 7: Report

Summarize what changed:

| Selector | Old (broken) | New (fixed) |
|---|---|---|
| Article URL | `a[itemprop="url"]` | `a.blogCardTitle_wog0` |
| Description | `meta[itemprop="description"]` | `p.blogCardDescription_Y1fO` |

Note any guard that was added, and confirm the feed now passes validation.

## Notes

- Never change the feed's `FEED_NAME`, `BLOG_URL`, or output filename — only fix selectors.
- If the site structure changed so drastically that no useful selectors remain, report back before rewriting — a full rewrite needs user sign-off.
- If the live HTML is a JavaScript SPA shell (near-empty body), note this explicitly — Selenium is required and this skill cannot fix it unattended.
- Clean up `/tmp/feed_fix_live.html` after verifying if desired.

