Just headlines

I rely on mostly web feeds to stay informed. Live Bookmarks was a Firefox feature I was fond of early on. Past Yahoo! Pipes and SimplePie, I learnt to live with Google Reader for a while. Then that service got retired of course, so I gave Digg and Feedly a quick try. Both felt less than homely and, similar to command line alternative Newsbeuter, too much for my needs. I had no use for tagging, searching, sharing, shortcuts, suggestions, summaries or media. I just wanted a preferably browser based helping of the latest headlines every so often if I could.

Please wait

headlines.js Helps read the news.

Time to get creative then. Putting together a dependency free aggregator in everyday PHP is surprisingly straightforward. As a first step, the following could be called to load, parse and display in-terminal snippets for a single RSS feed,

<?php
// Sample RSS 'parser.php' e.g.,
// php parser.php http://rss-feed-url > headlines.txt

$root = simplexml_load_file($argv[1], 'SimpleXMLElement');

// Failed to load feed xml, cut if atom for now
if (false === $root || 'feed' === $root->getName()) {
  die;
}

$tree = $root->xpath('/rss//item');

foreach ($tree as $node) {
  // Leave out the summaries, ad-lib for csv, tsv
  echo <<<EOT
$node->pubDate
$node->title
$node->link

EOT;
}

Expand for atom, apply over an array of sources, cron-schedule and that's base functionality taken care of evidently. A bit flimsy and kind of less portable a solution than my news addiction deserves no doubt. One might choose Go to produce a proper executable instead. Go would also allow for concurrent downloading and marshalling to e.g. JSON. Marvellous, what about the web facing part however? So I can reach for updates on mobile? Cloud deploy? If I were building a whole service maybe. Or else?

Well parsing XML in vanilla JavaScript is thankfully super easy. Utilising DOMParser and document.querySelector is literally all it takes. For example,

function parseFeed(text = 'The result of some fetch request', parser = new DOMParser()) {
  const root = parser.parseFromString(text, 'text/xml')
  const tree = root.querySelectorAll('item, entry')

  return Array.from(tree).map((node) => {
    // What a treat, I can search for node children using fallback selectors
    const { textContent: title } = node.querySelector('title, summary')
    // Need a `pubDate` for RSS
    const { textContent: date } = node.querySelector('updated, published, pubDate')
    // Expect an `href` attr with atom feeds
    const link = node.querySelector('link')

    return { date, title, link: link.getAttribute('href') || link.textContent }
  })
}

And considering how Web Components, Promise.all, template literals and a ServiceWorker backed window.fetch are now widely available, feed reading might after all be reduced to declaring an embeddable custom element,

<!-- client.html -->
<headlines-maybe src="http://cors-enabled-atom-or-rss-feed-url-1">
  <headlines-maybe src="http://cors-enabled-atom-or-rss-feed-url-2">
    <!-- Fetch each `src`, parse, merge and fill in Shadow DOM -->
  </headlines-maybe>
</headlines-maybe>
// module.js
customElements.define('headlines-maybe', class HeadlinesMaybe extends HTMLElement {})

Note how naturally HTML nesting covers joining multiple feeds,

class HeadlinesMaybe extends HTMLElement {
  // ...
  connectedCallback() {
    // Allow nesting, exclude child elements of the same type
    if (this.parentNode && this.parentNode.localName === this.localName) {
      return
    }

    // Make sure fetching avoided unless tag has context
    if (this.isConnected) {
      // Collect `src` urls, including self
      const children = this.querySelectorAll(this.localName)
      const sources = Array.from([this, ...children])
        .filter(o => o.hasAttribute('src'))
        .map(o => o.getAttribute('src'))

      this.render(sources)
    }
  }

  render(sources) {
    // Load, parse, merge and sort, and display results for each source
  }
}

Coupled with a stale-while-revalidate caching handler for the handful of feeds I'm interested in, I find loading times negligible. Module home is @thewhodidthis/headlines ›