diff --git a/bridges/CssSelectorFeedExpanderBridge.php b/bridges/CssSelectorFeedExpanderBridge.php
index 7e1f630f..008921b1 100644
--- a/bridges/CssSelectorFeedExpanderBridge.php
+++ b/bridges/CssSelectorFeedExpanderBridge.php
@@ -61,6 +61,10 @@ class CssSelectorFeedExpanderBridge extends CssSelectorBridge
         $discard_thumbnail = $this->getInput('discard_thumbnail');
         $limit = $this->getInput('limit');
 
+        //$xmlString = getContents($url);
+        //$feed = (new FeedParser())->parseFeed($xmlString);
+        //$items = $feed['items'];
+
         $feed_expander = new CssSelectorFeedExpanderBridgeInternal();
         $items = $feed_expander->collectExpandableDatas($url)->getItems();
 
diff --git a/bridges/FeedExpanderExampleBridge.php b/bridges/FeedExpanderExampleBridge.php
index a6b37f65..0e9ae386 100644
--- a/bridges/FeedExpanderExampleBridge.php
+++ b/bridges/FeedExpanderExampleBridge.php
@@ -46,21 +46,6 @@ class FeedExpanderExampleBridge extends FeedExpander
 
     protected function parseItem($newsItem)
     {
-        switch ($this->getInput('version')) {
-            case 'rss_0_9_1':
-                return $this->parseRss091Item($newsItem);
-                break;
-            case 'rss_1_0':
-                return $this->parseRss1Item($newsItem);
-                break;
-            case 'rss_2_0':
-                return $this->parseRss2Item($newsItem);
-                break;
-            case 'atom_1_0':
-                return $this->parseATOMItem($newsItem);
-                break;
-            default:
-                returnClientError('Unknown version ' . $this->getInput('version') . '!');
-        }
+        return (array) $newsItem;
     }
 }
diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php
index cae556b8..01d425b4 100644
--- a/bridges/MastodonBridge.php
+++ b/bridges/MastodonBridge.php
@@ -82,14 +82,14 @@ class MastodonBridge extends BridgeAbstract
         }
         $items = $content['orderedItems'] ?? $content['items'];
         foreach ($items as $status) {
-            $item = $this->parseItem($status);
+            $item = $this->parseStatus($status);
             if ($item) {
                 $this->items[] = $item;
             }
         }
     }
 
-    protected function parseItem($content)
+    protected function parseStatus($content)
     {
         $item = [];
         switch ($content['type']) {
diff --git a/bridges/NyaaTorrentsBridge.php b/bridges/NyaaTorrentsBridge.php
index 22225be8..217db377 100644
--- a/bridges/NyaaTorrentsBridge.php
+++ b/bridges/NyaaTorrentsBridge.php
@@ -67,7 +67,7 @@ class NyaaTorrentsBridge extends FeedExpander
 
     protected function parseItem($newItem)
     {
-        $item = parent::parseRss2Item($newItem);
+        $item = parent::parseItem($newItem);
         $item['id'] = str_replace(['https://nyaa.si/download/', '.torrent'], '', $item['uri']);
 
         $nyaaFields = (array)($newItem->children('nyaa', true));
diff --git a/bridges/RaceDepartmentBridge.php b/bridges/RaceDepartmentBridge.php
index c33ee67a..7fe92b4a 100644
--- a/bridges/RaceDepartmentBridge.php
+++ b/bridges/RaceDepartmentBridge.php
@@ -14,7 +14,7 @@ class RaceDepartmentBridge extends FeedExpander
 
     protected function parseItem($feedItem)
     {
-        $item = parent::parseRss2Item($feedItem);
+        $item = parent::parseItem($feedItem);
 
         //fetch page
         $articlePage = getSimpleHTMLDOMCached($feedItem->link);
diff --git a/index.php b/index.php
index 9181c0b0..123f6ecd 100644
--- a/index.php
+++ b/index.php
@@ -6,6 +6,56 @@ if (version_compare(\PHP_VERSION, '7.4.0') === -1) {
 
 require_once __DIR__ . '/lib/bootstrap.php';
 
+Configuration::verifyInstallation();
+$customConfig = [];
+if (file_exists(__DIR__ . '/config.ini.php')) {
+    $customConfig = parse_ini_file(__DIR__ . '/config.ini.php', true, INI_SCANNER_TYPED);
+}
+Configuration::loadConfiguration($customConfig, getenv());
+
+// Consider: ini_set('error_reporting', E_ALL & ~E_DEPRECATED);
+date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
+
 $rssBridge = new RssBridge();
 
+set_exception_handler(function (\Throwable $e) {
+    http_response_code(500);
+    print render(__DIR__ . '/templates/exception.html.php', ['e' => $e]);
+    RssBridge::getLogger()->error('Uncaught Exception', ['e' => $e]);
+    exit(1);
+});
+
+set_error_handler(function ($code, $message, $file, $line) {
+    if ((error_reporting() & $code) === 0) {
+        return false;
+    }
+    // In the future, uncomment this:
+    //throw new \ErrorException($message, 0, $code, $file, $line);
+    $text = sprintf(
+        '%s at %s line %s',
+        sanitize_root($message),
+        sanitize_root($file),
+        $line
+    );
+    RssBridge::getLogger()->warning($text);
+});
+
+// There might be some fatal errors which are not caught by set_error_handler() or \Throwable.
+register_shutdown_function(function () {
+    $error = error_get_last();
+    if ($error) {
+        $message = sprintf(
+            '(shutdown) %s: %s in %s line %s',
+            $error['type'],
+            sanitize_root($error['message']),
+            sanitize_root($error['file']),
+            $error['line']
+        );
+        RssBridge::getLogger()->error($message);
+        if (Debug::isEnabled()) {
+            print sprintf("<pre>%s</pre>\n", e($message));
+        }
+    }
+});
+
 $rssBridge->main($argv ?? []);
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index 76f570b6..70c4560d 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -1,95 +1,37 @@
 <?php
 
 /**
- * An abstract class for bridges that need to transform existing RSS or Atom
- * feeds.
- *
- * This class extends {@see BridgeAbstract} with functions to extract contents
- * from existing RSS or Atom feeds. Bridges that need to transform existing feeds
- * should inherit from this class instead of {@see BridgeAbstract}.
- *
- * Bridges that extend this class don't need to concern themselves with getting
- * contents from existing feeds, but can focus on adding additional contents
- * (i.e. by downloading additional data), filtering or just transforming a feed
- * into another format.
- *
- * @link http://www.rssboard.org/rss-0-9-1 RSS 0.91 Specification
- * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
- * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
- * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
- *
- * @todo The parsing functions should all be private. This class is complicated
- * enough without having to consider children overriding functions.
+ * Expands an existing feed
  */
 abstract class FeedExpander extends BridgeAbstract
 {
-    /** Indicates an RSS 1.0 feed */
     const FEED_TYPE_RSS_1_0 = 'RSS_1_0';
-
-    /** Indicates an RSS 2.0 feed */
     const FEED_TYPE_RSS_2_0 = 'RSS_2_0';
-
-    /** Indicates an Atom 1.0 feed */
     const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0';
 
-    /**
-     * Holds the title of the current feed
-     *
-     * @var string
-     */
-    private $title;
+    private string $feedType;
+    private FeedParser $feedParser;
+    private array $parsedFeed;
 
-    /**
-     * Holds the URI of the feed
-     *
-     * @var string
-     */
-    private $uri;
-
-    /**
-     * Holds the icon of the feed
-     *
-     */
-    private $icon;
-
-    /**
-     * Holds the feed type during internal operations.
-     *
-     * @var string
-     */
-    private $feedType;
-
-    /**
-     * Collects data from an existing feed.
-     *
-     * Children should call this function in {@see BridgeAbstract::collectData()}
-     * to extract a feed.
-     *
-     * @param string $url URL to the feed.
-     * @param int $maxItems Maximum number of items to collect from the feed
-     * (`-1`: no limit).
-     * @return self
-     */
-    public function collectExpandableDatas($url, $maxItems = -1)
+    public function __construct(CacheInterface $cache, Logger $logger)
     {
-        if (empty($url)) {
+        parent::__construct($cache, $logger);
+        $this->feedParser = new FeedParser();
+    }
+
+    public function collectExpandableDatas(string $url, $maxItems = -1)
+    {
+        if (!$url) {
             throw new \Exception('There is no $url for this RSS expander');
         }
-
-        Debug::log(sprintf('Loading from %s', $url));
-
-        /* Notice we do not use cache here on purpose:
-         * we want a fresh view of the RSS stream each time
-         */
-
-        $mimeTypes = [
-            MrssFormat::MIME_TYPE,
-            AtomFormat::MIME_TYPE,
-            '*/*',
-        ];
-        $httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)];
-        $xml = getContents($url, $httpHeaders);
-        if ($xml === '') {
+        if ($maxItems === -1) {
+            $maxItems = 999;
+        }
+        $accept = [MrssFormat::MIME_TYPE, AtomFormat::MIME_TYPE, '*/*'];
+        $httpHeaders = ['Accept: ' . implode(', ', $accept)];
+        // Notice we do not use cache here on purpose. We want a fresh view of the RSS stream each time
+        $xmlString = getContents($url, $httpHeaders);
+        if ($xmlString === '') {
             throw new \Exception(sprintf('Unable to parse xml from `%s` because we got the empty string', $url), 10);
         }
         // Maybe move this call earlier up the stack frames
@@ -97,8 +39,8 @@ abstract class FeedExpander extends BridgeAbstract
         libxml_use_internal_errors(true);
         // Consider replacing libxml with https://www.php.net/domdocument
         // Intentionally not using the silencing operator (@) because it has no effect here
-        $rssContent = simplexml_load_string(trim($xml));
-        if ($rssContent === false) {
+        $xml = simplexml_load_string(trim($xmlString));
+        if ($xml === false) {
             $xmlErrors = libxml_get_errors();
             foreach ($xmlErrors as $xmlError) {
                 Debug::log(trim($xmlError->message));
@@ -112,384 +54,62 @@ abstract class FeedExpander extends BridgeAbstract
         // Restore previous behaviour in case other code relies on it being off
         libxml_use_internal_errors(false);
 
-        // Commented out because it's spammy
-        // Debug::log(sprintf("RSS content is ===========\n%s===========", var_export($rssContent, true)));
+        // Currently only feed metadata (not items) are plucked out
+        $this->parsedFeed = $this->feedParser->parseFeed($xmlString);
 
-        switch (true) {
-            case isset($rssContent->item[0]):
-                Debug::log('Detected RSS 1.0 format');
-                $this->feedType = self::FEED_TYPE_RSS_1_0;
-                $this->collectRss1($rssContent, $maxItems);
-                break;
-            case isset($rssContent->channel[0]):
-                Debug::log('Detected RSS 0.9x or 2.0 format');
-                $this->feedType = self::FEED_TYPE_RSS_2_0;
-                $this->collectRss2($rssContent, $maxItems);
-                break;
-            case isset($rssContent->entry[0]):
-                Debug::log('Detected ATOM format');
-                $this->feedType = self::FEED_TYPE_ATOM_1_0;
-                $this->collectAtom1($rssContent, $maxItems);
-                break;
-            default:
-                Debug::log(sprintf('Unable to detect feed format from `%s`', $url));
-                throw new \Exception(sprintf('Unable to detect feed format from `%s`', $url));
+        if (isset($xml->item[0])) {
+            $this->feedType = self::FEED_TYPE_RSS_1_0;
+            $items = $xml->item;
+        } elseif (isset($xml->channel[0])) {
+            $this->feedType = self::FEED_TYPE_RSS_2_0;
+            $items = $xml->channel[0]->item;
+        } elseif (isset($xml->entry[0])) {
+            $this->feedType = self::FEED_TYPE_ATOM_1_0;
+            $items = $xml->entry;
+        } else {
+            throw new \Exception(sprintf('Unable to detect feed format from `%s`', $url));
+        }
+        foreach ($items as $item) {
+            $parsedItem = $this->parseItem($item);
+            if ($parsedItem) {
+                $this->items[] = $parsedItem;
+            }
+            if (count($this->items) >= $maxItems) {
+                break;
+            }
         }
-
         return $this;
     }
 
     /**
-     * Collect data from an RSS 1.0 compatible feed
-     *
-     * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
-     *
-     * @param string $rssContent The RSS content
-     * @param int $maxItems Maximum number of items to collect from the feed
-     * (`-1`: no limit).
-     * @return void
-     *
-     * @todo Instead of passing $maxItems to all functions, just add all items
-     * and remove excessive items later.
-     */
-    protected function collectRss1($rssContent, $maxItems)
-    {
-        $this->loadRss2Data($rssContent->channel[0]);
-        foreach ($rssContent->item as $item) {
-            $tmp_item = $this->parseItem($item);
-            if (!empty($tmp_item)) {
-                $this->items[] = $tmp_item;
-            }
-            if ($maxItems !== -1 && count($this->items) >= $maxItems) {
-                break;
-            }
-        }
-    }
-
-    /**
-     * Collect data from a RSS 2.0 compatible feed
-     *
-     * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
-     *
-     * @param int $maxItems Maximum number of items to collect from the feed (`-1`: no limit).
-     * @return void
-     *
-     * @todo Instead of passing $maxItems to all functions, just add all items and remove excessive items later.
-     */
-    protected function collectRss2(\SimpleXMLElement $rssContent, $maxItems)
-    {
-        $rssContent = $rssContent->channel[0];
-        $this->loadRss2Data($rssContent);
-        foreach ($rssContent->item as $item) {
-            $tmp_item = $this->parseItem($item);
-            if (!empty($tmp_item)) {
-                $this->items[] = $tmp_item;
-            }
-            if ($maxItems !== -1 && count($this->items) >= $maxItems) {
-                break;
-            }
-        }
-    }
-
-    /**
-     * Collect data from a Atom 1.0 compatible feed
-     *
-     * @link https://tools.ietf.org/html/rfc4287  The Atom Syndication Format
-     *
-     * @param object $content The Atom content
-     * @param int $maxItems Maximum number of items to collect from the feed
-     * (`-1`: no limit).
-     * @return void
-     *
-     * @todo Instead of passing $maxItems to all functions, just add all items
-     * and remove excessive items later.
-     */
-    protected function collectAtom1($content, $maxItems)
-    {
-        $this->loadAtomData($content);
-        foreach ($content->entry as $item) {
-            $tmp_item = $this->parseItem($item);
-            if (!empty($tmp_item)) {
-                $this->items[] = $tmp_item;
-            }
-            if ($maxItems !== -1 && count($this->items) >= $maxItems) {
-                break;
-            }
-        }
-    }
-
-    /**
-     * Load RSS 2.0 feed data into RSS-Bridge
-     *
-     * @param object $rssContent The RSS content
-     * @return void
-     *
-     * @todo set title, link, description, language, and so on
-     */
-    protected function loadRss2Data($rssContent)
-    {
-        $this->title = trim((string)$rssContent->title);
-        $this->uri = trim((string)$rssContent->link);
-
-        if (!empty($rssContent->image)) {
-            $this->icon = trim((string)$rssContent->image->url);
-        }
-    }
-
-    /**
-     * Load Atom feed data into RSS-Bridge
-     *
-     * @param object $content The Atom content
-     * @return void
-     */
-    protected function loadAtomData($content)
-    {
-        $this->title = (string)$content->title;
-
-        // Find best link (only one, or first of 'alternate')
-        if (!isset($content->link)) {
-            $this->uri = '';
-        } elseif (count($content->link) === 1) {
-            $this->uri = (string)$content->link[0]['href'];
-        } else {
-            $this->uri = '';
-            foreach ($content->link as $link) {
-                if (strtolower($link['rel']) === 'alternate') {
-                    $this->uri = (string)$link['href'];
-                    break;
-                }
-            }
-        }
-
-        if (!empty($content->icon)) {
-            $this->icon = (string)$content->icon;
-        } elseif (!empty($content->logo)) {
-            $this->icon = (string)$content->logo;
-        }
-    }
-
-    /**
-     * Parse the contents of a single Atom feed item into a RSS-Bridge item for
-     * further transformation.
-     *
-     * @param object $feedItem A single feed item
-     * @return object The RSS-Bridge item
-     *
-     * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
-     * of its own?
-     */
-    protected function parseATOMItem($feedItem)
-    {
-        // Some ATOM entries also contain RSS 2.0 fields
-        $item = $this->parseRss2Item($feedItem);
-
-        if (isset($feedItem->id)) {
-            $item['uri'] = (string)$feedItem->id;
-        }
-        if (isset($feedItem->title)) {
-            $item['title'] = html_entity_decode((string)$feedItem->title);
-        }
-        if (isset($feedItem->updated)) {
-            $item['timestamp'] = strtotime((string)$feedItem->updated);
-        }
-        if (isset($feedItem->author)) {
-            $item['author'] = (string)$feedItem->author->name;
-        }
-        if (isset($feedItem->content)) {
-            $contentChildren = $feedItem->content->children();
-            if (count($contentChildren) > 0) {
-                $content = '';
-                foreach ($contentChildren as $contentChild) {
-                    $content .= $contentChild->asXML();
-                }
-                $item['content'] = $content;
-            } else {
-                $item['content'] = (string)$feedItem->content;
-            }
-        }
-
-        //When "link" field is present, URL is more reliable than "id" field
-        if (count($feedItem->link) === 1) {
-            $item['uri'] = (string)$feedItem->link[0]['href'];
-        } else {
-            foreach ($feedItem->link as $link) {
-                if (strtolower($link['rel']) === 'alternate') {
-                    $item['uri'] = (string)$link['href'];
-                }
-                if (strtolower($link['rel']) === 'enclosure') {
-                    $item['enclosures'][] = (string)$link['href'];
-                }
-            }
-        }
-
-        return $item;
-    }
-
-    /**
-     * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item
-     * for further transformation.
-     *
-     * @param object $feedItem A single feed item
-     * @return object The RSS-Bridge item
-     *
-     * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
-     * of its own?
-     */
-    protected function parseRss091Item($feedItem)
-    {
-        $item = [];
-        if (isset($feedItem->link)) {
-            $item['uri'] = (string)$feedItem->link;
-        }
-        if (isset($feedItem->title)) {
-            $item['title'] = html_entity_decode((string)$feedItem->title);
-        }
-        // rss 0.91 doesn't support timestamps
-        // rss 0.91 doesn't support authors
-        // rss 0.91 doesn't support enclosures
-        if (isset($feedItem->description)) {
-            $item['content'] = (string)$feedItem->description;
-        }
-        return $item;
-    }
-
-    /**
-     * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item
-     * for further transformation.
-     *
-     * @param object $feedItem A single feed item
-     * @return object The RSS-Bridge item
-     *
-     * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
-     * of its own?
-     */
-    protected function parseRss1Item($feedItem)
-    {
-        // 1.0 adds optional elements around the 0.91 standard
-        $item = $this->parseRss091Item($feedItem);
-
-        $namespaces = $feedItem->getNamespaces(true);
-        if (isset($namespaces['dc'])) {
-            $dc = $feedItem->children($namespaces['dc']);
-            if (isset($dc->date)) {
-                $item['timestamp'] = strtotime((string)$dc->date);
-            }
-            if (isset($dc->creator)) {
-                $item['author'] = (string)$dc->creator;
-            }
-        }
-
-        return $item;
-    }
-
-    /**
-     * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item
-     * for further transformation.
-     *
-     * @param object $feedItem A single feed item
-     * @return object The RSS-Bridge item
-     *
-     * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
-     * of its own?
-     */
-    protected function parseRss2Item($feedItem)
-    {
-        // Primary data is compatible to 0.91 with some additional data
-        $item = $this->parseRss091Item($feedItem);
-
-        $namespaces = $feedItem->getNamespaces(true);
-        if (isset($namespaces['dc'])) {
-            $dc = $feedItem->children($namespaces['dc']);
-        }
-        if (isset($namespaces['media'])) {
-            $media = $feedItem->children($namespaces['media']);
-        }
-
-        if (isset($feedItem->guid)) {
-            foreach ($feedItem->guid->attributes() as $attribute => $value) {
-                if (
-                    $attribute === 'isPermaLink'
-                    && (
-                        $value === 'true' || (
-                            filter_var($feedItem->guid, FILTER_VALIDATE_URL)
-                            && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL))
-                        )
-                    )
-                ) {
-                    $item['uri'] = (string)$feedItem->guid;
-                    break;
-                }
-            }
-        }
-
-        if (isset($feedItem->pubDate)) {
-            $item['timestamp'] = strtotime((string)$feedItem->pubDate);
-        } elseif (isset($dc->date)) {
-            $item['timestamp'] = strtotime((string)$dc->date);
-        }
-
-        if (isset($feedItem->author)) {
-            $item['author'] = (string)$feedItem->author;
-        } elseif (isset($feedItem->creator)) {
-            $item['author'] = (string)$feedItem->creator;
-        } elseif (isset($dc->creator)) {
-            $item['author'] = (string)$dc->creator;
-        } elseif (isset($media->credit)) {
-            $item['author'] = (string)$media->credit;
-        }
-
-        if (isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) {
-            $item['enclosures'] = [(string)$feedItem->enclosure['url']];
-        }
-
-        return $item;
-    }
-
-    /**
-     * Parse the contents of a single feed item, depending on the current feed
-     * type, into a RSS-Bridge item.
-     *
-     * @param object $item The current feed item
-     * @return object A RSS-Bridge item, with (hopefully) the whole content
+     * @param \SimpleXMLElement $item The feed item to be parsed
      */
     protected function parseItem($item)
     {
         switch ($this->feedType) {
             case self::FEED_TYPE_RSS_1_0:
-                return $this->parseRss1Item($item);
+                return $this->feedParser->parseRss1Item($item);
             case self::FEED_TYPE_RSS_2_0:
-                return $this->parseRss2Item($item);
+                return $this->feedParser->parseRss2Item($item);
             case self::FEED_TYPE_ATOM_1_0:
-                return $this->parseATOMItem($item);
+                return $this->feedParser->parseAtomItem($item);
             default:
                 throw new \Exception(sprintf('Unknown version %s!', $this->getInput('version')));
         }
     }
 
-    /** {@inheritdoc} */
     public function getURI()
     {
-        if (!empty($this->uri)) {
-            return $this->uri;
-        }
-        return parent::getURI();
+        return $this->parsedFeed['uri'] ?? parent::getURI();
     }
 
-    /** {@inheritdoc} */
     public function getName()
     {
-        if (!empty($this->title)) {
-            return $this->title;
-        }
-        return parent::getName();
+        return $this->parsedFeed['title'] ?? parent::getName();
     }
 
-    /** {@inheritdoc} */
     public function getIcon()
     {
-        if (!empty($this->icon)) {
-            return $this->icon;
-        }
-        return parent::getIcon();
+        return $this->parsedFeed['icon'] ?? parent::getIcon();
     }
 }
diff --git a/lib/FeedParser.php b/lib/FeedParser.php
new file mode 100644
index 00000000..90df548d
--- /dev/null
+++ b/lib/FeedParser.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+final class FeedParser
+{
+    public function parseFeed(string $xmlString): array
+    {
+        $xml = simplexml_load_string(trim($xmlString));
+        if ($xml === false) {
+            throw new \Exception('Unable to parse xml');
+        }
+        $feed = [
+            'title' => null,
+            'url'   => null,
+            'icon'  => null,
+            'items' => [],
+        ];
+        if (isset($xml->item[0])) {
+            // rss 1.0
+            $channel = $xml->channel[0];
+            $feed['title'] = trim((string)$channel->title);
+            $feed['uri'] = trim((string)$channel->link);
+            if (!empty($channel->image)) {
+                $feed['icon'] = trim((string)$channel->image->url);
+            }
+            foreach ($xml->item as $item) {
+                $feed['items'][] = $this->parseRss1Item($item);
+            }
+        } elseif (isset($xml->channel[0])) {
+            // rss 2.0
+            $channel = $xml->channel[0];
+            $feed['title'] = trim((string)$channel->title);
+            $feed['uri'] = trim((string)$channel->link);
+            if (!empty($channel->image)) {
+                $feed['icon'] = trim((string)$channel->image->url);
+            }
+            foreach ($channel->item as $item) {
+                $feed['items'][] = $this->parseRss2Item($item);
+            }
+        } elseif (isset($xml->entry[0])) {
+            // atom 1.0
+            $feed['title'] = (string)$xml->title;
+            // Find best link (only one, or first of 'alternate')
+            if (!isset($xml->link)) {
+                $feed['uri'] = '';
+            } elseif (count($xml->link) === 1) {
+                $feed['uri'] = (string)$xml->link[0]['href'];
+            } else {
+                $feed['uri'] = '';
+                foreach ($xml->link as $link) {
+                    if (strtolower((string) $link['rel']) === 'alternate') {
+                        $feed['uri'] = (string)$link['href'];
+                        break;
+                    }
+                }
+            }
+            if (!empty($xml->icon)) {
+                $feed['icon'] = (string)$xml->icon;
+            } elseif (!empty($xml->logo)) {
+                $feed['icon'] = (string)$xml->logo;
+            }
+            foreach ($xml->entry as $item) {
+                $feed['items'][] = $this->parseAtomItem($item);
+            }
+        } else {
+            throw new \Exception(sprintf('Unable to detect feed format from `%s`', $url));
+        }
+
+        return $feed;
+    }
+
+    public function parseAtomItem(\SimpleXMLElement $feedItem): array
+    {
+        // Some ATOM entries also contain RSS 2.0 fields
+        $item = $this->parseRss2Item($feedItem);
+
+        if (isset($feedItem->id)) {
+            $item['uri'] = (string)$feedItem->id;
+        }
+        if (isset($feedItem->title)) {
+            $item['title'] = html_entity_decode((string)$feedItem->title);
+        }
+        if (isset($feedItem->updated)) {
+            $item['timestamp'] = strtotime((string)$feedItem->updated);
+        }
+        if (isset($feedItem->author)) {
+            $item['author'] = (string)$feedItem->author->name;
+        }
+        if (isset($feedItem->content)) {
+            $contentChildren = $feedItem->content->children();
+            if (count($contentChildren) > 0) {
+                $content = '';
+                foreach ($contentChildren as $contentChild) {
+                    $content .= $contentChild->asXML();
+                }
+                $item['content'] = $content;
+            } else {
+                $item['content'] = (string)$feedItem->content;
+            }
+        }
+
+        // When "link" field is present, URL is more reliable than "id" field
+        if (count($feedItem->link) === 1) {
+            $item['uri'] = (string)$feedItem->link[0]['href'];
+        } else {
+            foreach ($feedItem->link as $link) {
+                if (strtolower((string) $link['rel']) === 'alternate') {
+                    $item['uri'] = (string)$link['href'];
+                }
+                if (strtolower((string) $link['rel']) === 'enclosure') {
+                    $item['enclosures'][] = (string)$link['href'];
+                }
+            }
+        }
+        return $item;
+    }
+
+    public function parseRss2Item(\SimpleXMLElement $feedItem): array
+    {
+        // Primary data is compatible to 0.91 with some additional data
+        $item = $this->parseRss091Item($feedItem);
+
+        $namespaces = $feedItem->getNamespaces(true);
+        if (isset($namespaces['dc'])) {
+            $dc = $feedItem->children($namespaces['dc']);
+        }
+        if (isset($namespaces['media'])) {
+            $media = $feedItem->children($namespaces['media']);
+        }
+
+        if (isset($feedItem->guid)) {
+            foreach ($feedItem->guid->attributes() as $attribute => $value) {
+                if (
+                    $attribute === 'isPermaLink'
+                    && (
+                        $value === 'true' || (
+                            filter_var($feedItem->guid, FILTER_VALIDATE_URL)
+                            && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL))
+                        )
+                    )
+                ) {
+                    $item['uri'] = (string)$feedItem->guid;
+                    break;
+                }
+            }
+        }
+
+        if (isset($feedItem->pubDate)) {
+            $item['timestamp'] = strtotime((string)$feedItem->pubDate);
+        } elseif (isset($dc->date)) {
+            $item['timestamp'] = strtotime((string)$dc->date);
+        }
+
+        if (isset($feedItem->author)) {
+            $item['author'] = (string)$feedItem->author;
+        } elseif (isset($feedItem->creator)) {
+            $item['author'] = (string)$feedItem->creator;
+        } elseif (isset($dc->creator)) {
+            $item['author'] = (string)$dc->creator;
+        } elseif (isset($media->credit)) {
+            $item['author'] = (string)$media->credit;
+        }
+
+        if (isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) {
+            $item['enclosures'] = [(string)$feedItem->enclosure['url']];
+        }
+        return $item;
+    }
+
+    public function parseRss1Item(\SimpleXMLElement $feedItem): array
+    {
+        // 1.0 adds optional elements around the 0.91 standard
+        $item = $this->parseRss091Item($feedItem);
+        $namespaces = $feedItem->getNamespaces(true);
+        if (isset($namespaces['dc'])) {
+            $dc = $feedItem->children($namespaces['dc']);
+            if (isset($dc->date)) {
+                $item['timestamp'] = strtotime((string)$dc->date);
+            }
+            if (isset($dc->creator)) {
+                $item['author'] = (string)$dc->creator;
+            }
+        }
+        return $item;
+    }
+
+    public function parseRss091Item(\SimpleXMLElement $feedItem): array
+    {
+        $item = [];
+        if (isset($feedItem->link)) {
+            $item['uri'] = (string)$feedItem->link;
+        }
+        if (isset($feedItem->title)) {
+            $item['title'] = html_entity_decode((string)$feedItem->title);
+        }
+        // rss 0.91 doesn't support timestamps
+        // rss 0.91 doesn't support authors
+        // rss 0.91 doesn't support enclosures
+        if (isset($feedItem->description)) {
+            $item['content'] = (string)$feedItem->description;
+        }
+        return $item;
+    }
+}
diff --git a/lib/RssBridge.php b/lib/RssBridge.php
index 2fb21323..d56c59b8 100644
--- a/lib/RssBridge.php
+++ b/lib/RssBridge.php
@@ -8,66 +8,13 @@ final class RssBridge
 
     public function __construct()
     {
-        Configuration::verifyInstallation();
-
-        $customConfig = [];
-        if (file_exists(__DIR__ . '/../config.ini.php')) {
-            $customConfig = parse_ini_file(__DIR__ . '/../config.ini.php', true, INI_SCANNER_TYPED);
-        }
-        Configuration::loadConfiguration($customConfig, getenv());
-
-        // Consider: ini_set('error_reporting', E_ALL & ~E_DEPRECATED);
-        date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
-
-        set_exception_handler(function (\Throwable $e) {
-            self::$logger->error('Uncaught Exception', ['e' => $e]);
-            http_response_code(500);
-            print render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]);
-            exit(1);
-        });
-
-        set_error_handler(function ($code, $message, $file, $line) {
-            if ((error_reporting() & $code) === 0) {
-                return false;
-            }
-            // In the future, uncomment this:
-            //throw new \ErrorException($message, 0, $code, $file, $line);
-            $text = sprintf(
-                '%s at %s line %s',
-                sanitize_root($message),
-                sanitize_root($file),
-                $line
-            );
-            self::$logger->warning($text);
-        });
-
-        // There might be some fatal errors which are not caught by set_error_handler() or \Throwable.
-        register_shutdown_function(function () {
-            $error = error_get_last();
-            if ($error) {
-                $message = sprintf(
-                    '(shutdown) %s: %s in %s line %s',
-                    $error['type'],
-                    sanitize_root($error['message']),
-                    sanitize_root($error['file']),
-                    $error['line']
-                );
-                self::$logger->error($message);
-                if (Debug::isEnabled()) {
-                    print sprintf("<pre>%s</pre>\n", e($message));
-                }
-            }
-        });
-
         self::$logger = new SimpleLogger('rssbridge');
         if (Debug::isEnabled()) {
             self::$logger->addHandler(new StreamHandler(Logger::DEBUG));
         } else {
             self::$logger->addHandler(new StreamHandler(Logger::INFO));
         }
-
         self::$httpClient = new CurlHttpClient();
-
         $cacheFactory = new CacheFactory(self::$logger);
         if (Debug::isEnabled()) {
             self::$cache = $cacheFactory->create('array');