Feed Downloader
Web Development

Feed Downloader

webdesignNext.jsNo backendNo databasefeedutility

Menší web s jasným účelem. Firemním standardem pro sdílení obrázků e-shopů, jsou takzvané feedy. jejich stažení vyžaduje aby se člověk dostal na stránku feedu, našel adresu na obrázek, otevřel ji, dal uložit a soubor nějak pojmenoval. Ale z logiky věci, feedy nejsou tvořeny lidmi pro lidi. Jsou to skriptově generované stránky určené pro script, který je zvládne stáhnout. a přesně takový nástroj je tento web, web obsahuje i desktopovou aplikaci v pythonu pro stahování wětších feedů a automatické ukládání. aktuální vezme má některý prohlížečích se stahováním po jednom, prosím zvolte zip, na opravě se bude v budoucnu pracovat

// Jak to funguje

Proč je to zajímavější než obyčejný RSS reader

Feedy nejsou jeden standard, spíš divoký západ. Obrázek může být v enclosure, media:content, media:thumbnail, content:encoded, v description nebo úplně jinde v JSON Feedu. FeedDown proto vstup nejdřív uklidí a normalizuje a až potom ho analyzuje.

Feedy nemají jednotný tvar, takže se vstup rozpadá na čtyři různé problémy. Každý z nich řeší jiná část appky.

INPUT

Neznámý zdroj

URL může vrátit RSS, Atom, JSON nebo dokonce HTML.

PARSING

Více formátů

Parser má oddělené větve pro RSS, Atom a JSON Feed.

MEDIA

Image discovery

Obrázky se hledají ve více standardních i vložených strukturách.

OUTPUT

Analýza / export

Výsledkem není jen náhled, ale strukturovaná data použitelná dál.

Appka proto nejdřív všechno sjednotí a až pak analyzuje. Jak ta cesta vypadá krok po kroku, ukazuje další schéma.

// Jak to funguje

Datový tok

URL→Proxy→Raw feed→Parser→Normalized items→Analysis / export

Rozdělení je záměrné. Síť řeší fetching, formát řeší parser a další vrstva už dostává čisté položky. Díky tomu se exportní logika nemusí trápit XML syntaxí.

// Kousek kódu

Proxy řeší problém, který UI samo nevyřeší

KÓD 01 · FEED PROXYsrc/app/api/feed-proxy/route.js
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const feedUrl = searchParams.get("url");

  if (!feedUrl) {
    return Response.json({ error: "Chybí parametr url." }, { status: 400 });
  }

  let parsed;
  try {
    parsed = new URL(feedUrl);
    if (!parsed.protocol.startsWith("http")) {
      throw new Error("invalid protocol");
    }
  } catch {
    return Response.json({ error: "Neplatná URL feedu." }, { status: 400 });
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 15000);

  try {
    const resp = await fetch(parsed.toString(), {
      signal: controller.signal,
      redirect: "follow",
      headers: {
        "User-Agent": "FeedDown/1.0 (+https://FeedDown.app)",
        Accept: "application/rss+xml, application/atom+xml, application/json, text/xml, application/xml, text/plain, */*",
      },
    });
Co to děláValiduje URL, nastavuje timeout, stáhne feed na serveru a předá klientovi i původní content type.
Proč na tom záležíProhlížeč nemůže spolehlivě načítat libovolné cizí feedy kvůli CORS. Proxy zároveň poskytuje kontrolovaný timeout a převádí síťové chyby na srozumitelnou odpověď.
// Kousek kódu

Parser jako hlavní logická vrstva

KÓD 02 · FORMAT DETECTIONsrc/lib/feedParser.js
export function parseFeed(content, contentType) {
  const trimmed = content.trim();

  if (trimmed.startsWith("{") || contentType.includes("application/json")) {
    try {
      const data = JSON.parse(trimmed);
      return parseJsonFeed(data);
    } catch {
      // fall through to XML
    }
  }

  const parser = new DOMParser();
  const xmlDoc = parser.parseFromString(content, "text/xml");

  const parserError = xmlDoc.querySelector("parsererror");
  if (parserError && !xmlDoc.querySelector("rss") && !xmlDoc.querySelector("feed")) {
    const htmlDoc = parser.parseFromString(content, "text/html");
    const rssLink = htmlDoc.querySelector('link[type="application/rss+xml"]');
    if (rssLink) {
      throw new Error(`Toto je HTML stránka, ne feed. RSS odkaz nalezen: ${rssLink.getAttribute("href")}`);
    }
    throw new Error("Feed se nepodařilo rozparsovat. Obsah není platný RSS, Atom ani JSON Feed.");
  }
Co to děláRozhoduje, zda vstup vypadá jako JSON Feed nebo XML, následně kontroluje RSS/Atom a dává smysluplnou chybu při HTML či neplatném dokumentu.
Proč na tom záležíTady je skutečný problém projektu: ne každý „feed URL“ je validní feed. Parser proto nesmí pouze selhat na prvním selectoru; musí rozlišit typ vstupu a vysvětlit, proč data nejsou zpracovatelná.
// Kousek kódu

Fetch a parsing jsou oddělené

KÓD 03 · FETCH → PARSE → ANALYZEsrc/lib/feedFetcher.js
export async function fetchAndAnalyzeFeed(feedUrl) {
  const proxiedUrl = `/api/feed-proxy?url=${encodeURIComponent(feedUrl)}`;

  const resp = await fetch(proxiedUrl);

  if (!resp.ok) {
    let message = `Feed se nepodařilo načíst (HTTP ${resp.status}). Zkuste jiný feed.`;
    try {
      const data = await resp.json();
      if (data?.error) message = data.error;
    } catch {
      // response wasn't JSON, keep default message
    }
    throw new Error(message);
  }

  const contentType = resp.headers.get("x-original-content-type") || "";
  const text = await resp.text();

  if (!text || text.trim().length === 0) {
    throw new Error("Feed je prázdný nebo se nepodařilo načíst obsah.");
  }

  const parsed = parseFeed(text, contentType);
  return analyzeFeed(parsed);
}
Co to děláNačte proxied response, zkontroluje HTTP stav a prázdný obsah, předá content type parseru a výsledek analyzátoru.
Proč na tom záležíJe to tenký orchestration layer. Díky tomu zůstává síťová komunikace oddělená od samotného parsování a případná chyba má jasné místo.
// Jak to funguje

Co bylo potřeba vyřešit v parseru

Nejtěžší nebyla syntaxe DOMParseru, ale množství variant pro tu samou věc. Jeden feed dá obrázek do atributu, druhý do HTML uvnitř description. Parser proto skládá jednotný seznam imageSources a u každého obrázku si pamatuje, odkud se vzal.

Stejná věc se v každém formátu zapisuje jinak. Tady je přehled variant, které parser musí umět přečíst.

RSS

item + enclosure

Klasický feed s přílohami.

MEDIA RSS

media:content

Namespace-based media data.

ATOM

entry + link

Jiný model položky a odkazu.

JSON

content_html + attachments

Struktura mimo XML.

Všechny větve končí ve stejném seznamu obrázků i s informací, odkud přišly. Exportu je pak jedno, z jakého formátu data původně jsou.

// Jak to funguje

Export a práce s výsledkem

V repu je downloader a XLSX závislosti. To dává smysl až po normalizaci, protože exportu je jedno, jestli data původně přišla z RSS, nebo z JSONu.

// Jak to funguje

Význam projektu

FeedDown je skvělý příklad appky, kde frontend není hlavní problém. UI může být jednoduché, ale pod ním je pipeline, která musí přežít nepořádek na vstupu a přitom vrátit pořádek na výstupu.

// Pod kapotou

Z čeho je projekt postavený

Žádná technologie tu není jen pro parádu. Tady je rychlý přehled, kdo za co odpovídá.

FRONTEND + API

Next.js 16

JAZYK

JavaScript

VSTUPNÍ FORMÁTY

RSS / Atom / JSON Feed

SERVER-SIDE FETCH

Proxy API

PARSOVÁNÍ FEEDU

XML DOMParser

EXPORT

XLSX + ZIP

// Shrnutí

Co je na projektu podstatné?

Nejde mi jen o to, jak to vypadá. Baví mě hlavně to, co je pod tím: kudy data tečou, co se validuje, co může selhat a proč jsem to udělala právě takhle.

Napiš mi