Next.js

RSS n'est pas mort. J'en ai construit la preuve.

Pourquoi j'ai codé mon propre agrégateur RSS avec Next.js — et ce que le projet m'a appris sur le CORS, l'hydratation, et les API routes.

RSS n'est pas mort. J'en ai construit la preuve.

RSS a 25 ans. Il devrait être mort depuis longtemps, remplacé par des algorithmes et des feeds personnalisés. Il ne l'est pas. Moi, je l'utilise tous les jours pour faire de la veille technologique avec RSS — blogs de développeurs, changelogs de librairies, newsletters techniques. Et un jour, j'ai eu envie de contrôler l'outil plutôt que de dépendre d'un service tiers.

C'est comme ça qu'OpenRss est né — pas parce que les alternatives n'existent pas, mais parce que construire l'outil soi-même, c'est aussi apprendre comment il fonctionne.

Le problème que j'avais ignoré : CORS

Mon premier réflexe a été de parser les feeds directement dans le navigateur. Simple, non ? Tu récupères le XML avec fetch, tu le parses, tu affiches les articles.

Ça ne marche pas. Le CORS bloque quasiment tous les feeds RSS côté client. Les serveurs qui hébergent des feeds ne configurent pas leurs headers pour autoriser des requêtes depuis un domaine tiers. C'est leur droit — ils n'ont aucune raison de le faire.

La solution évidente : déplacer le parsing côté serveur. Next.js rend ça trivial avec les API routes. Deux endpoints, et le problème disparaît.

app/api/rss/parse/route.ts
import Parser from 'rss-parser'
 
const parser = new Parser()
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const url = searchParams.get('url')
 
  if (!url) return Response.json({ error: 'URL required' }, { status: 400 })
 
  const feed = await parser.parseURL(url)
  return Response.json(feed)
}

Le serveur Next.js fait la requête vers le feed RSS — pas le navigateur. Zéro problème de CORS. Le client parle uniquement à ton propre serveur.

J'ai séparé en deux endpoints : /api/rss/parse pour un seul feed, /api/rss/parse-multiple pour charger plusieurs sources en parallèle au démarrage. La différence de performance est visible — charger 5 feeds en série vs en parallèle, c'est 5x plus lent sans raison.

L'hydratation m'a pris une heure

L'idée de persister les feeds de l'utilisateur dans localStorage semblait simple. Tu sauvegardes, tu recharges, tu réhydrates. Sauf que Next.js rend les pages côté serveur, et localStorage n'existe pas sur le serveur.

La page s'affichait avec les feeds par défaut côté serveur, puis "flashait" vers les feeds sauvegardés côté client. React détectait la divergence et envoyait une erreur d'hydratation dans la console — un concept fondamental du rendu serveur.

hooks/useFeedStore.ts
import { useSyncExternalStore } from 'react'
 
function subscribe(callback: () => void) {
  window.addEventListener('storage', callback)
  return () => window.removeEventListener('storage', callback)
}
 
function getSnapshot() {
  const stored = localStorage.getItem('feeds')
  return stored ? JSON.parse(stored) : DEFAULT_FEEDS
}
 
function getServerSnapshot() {
  return DEFAULT_FEEDS
}
 
export function useFeedStore() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}

useSyncExternalStore prend trois arguments : subscribe, getSnapshot (client), getServerSnapshot (serveur). Le serveur retourne toujours les feeds par défaut. Le client retourne ce qui est dans localStorage. React ne voit plus de divergence, pas d'erreur.

C'est l'API React officielle pour synchroniser un store externe avec le rendu — et je l'avais ignorée depuis sa sortie. Ce projet m'a forcé à vraiment la comprendre.

La gestion des sources

L'interface de gestion des feeds est un drawer — un panneau latéral qui s'ouvre sur le côté. Pas de page dédiée, pas de navigation. Tu ajoutes une URL, le feed se parse en temps réel pour vérifier qu'il est valide, et il s'ajoute à ta liste.

J'ai utilisé HeadlessUI pour l'accessibilité. Gérer le focus, l'échappement au clavier, l'aria-label correct sur un drawer custom — c'est beaucoup de boilerplate pour pas grand chose de visible. HeadlessUI encapsule tout ça proprement.

Ce que j'ai appris : la validation d'un feed RSS ne se fait pas avec une regex sur l'URL. Tu le sais seulement après l'avoir parsé. Donc la validation est asynchrone — tu envoies l'URL à ton endpoint, s'il répond avec un feed valide, tu l'ajoutes. Sinon, tu montres une erreur.

components/FeedDrawer.tsx
async function validateAndAdd(url: string) {
  try {
    const res = await fetch(`/api/rss/parse?url=${encodeURIComponent(url)}`)
    if (!res.ok) throw new Error()
    const feed = await res.json()
    addFeed({ url, title: feed.title })
  } catch {
    setError("Ce feed n'est pas valide ou inaccessible.")
  }
}

La recherche en temps réel

La recherche filtre les articles par titre, côté client, sans aucune requête serveur. Le state contient tous les articles déjà chargés — la recherche ne fait que filtrer l'affichage.

hooks/useArticleSearch.ts
export function useArticleSearch(articles: Article[], query: string) {
  return useMemo(
    () => articles.filter(a =>
      a.title.toLowerCase().includes(query.toLowerCase())
    ),
    [articles, query]
  )
}

Mémoïsé pour éviter de refiltrer à chaque render. Sur des centaines d'articles, c'est la différence entre une UI fluide et des lags visibles.

Ce que ce projet m'a vraiment appris

J'aurais pu utiliser Feedly, Inoreader, ou n'importe quel autre agrégateur existant. Le mien est moins complet, moins fiable, moins maintenu. C'est assumé.

Construire OpenRss m'a obligé à résoudre des problèmes que je connaissais en théorie mais que je n'avais jamais eu à affronter directement. Le CORS côté client, c'est le genre de truc que tu lis dans une doc et que tu oublies jusqu'au jour où il bloque ton projet. useSyncExternalStore, pareil — j'aurais pu finir par le "réinventer" avec un useEffect bancal si je n'avais pas été forcé de chercher la bonne solution.

La prochaine étape : détecter automatiquement les feeds RSS depuis une URL de site web — c'est-à-dire parser le <link rel="alternate"> dans le HTML d'une page pour extraire l'URL du feed sans que l'utilisateur ait à la connaître. La plupart des blogs l'exposent, la plupart des gens ne savent pas où chercher.

Le code est sur GitHub si tu veux regarder l'implémentation complète.

Écrit par William Loree