diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index d2c07afeb0380e60809f108895afe39d06423a78..f7a908a51a9bb1c04dc059823082a8413d7d4453 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -143,9 +143,7 @@ function aggregator_menu() {
   );
   $items['aggregator/categories'] = array(
     'title' => 'Categories',
-    'page callback' => 'aggregator_page_categories',
-    'access callback' => '_aggregator_has_categories',
-    'file' => 'aggregator.pages.inc',
+    'route_name' => 'aggregator_categories',
   );
   $items['aggregator/rss'] = array(
     'title' => 'RSS feed',
@@ -254,17 +252,6 @@ function _aggregator_category_title($category) {
   return $category->title;
 }
 
-/**
- * Access callback: Determines whether there are any aggregator categories.
- *
- * @return
- *   TRUE if there is at least one category and the user has access to them;
- *   FALSE otherwise.
- */
-function _aggregator_has_categories() {
-  return user_access('access news feeds') && (bool) db_query_range('SELECT 1 FROM {aggregator_category}', 0, 1)->fetchField();
-}
-
 /**
  * Implements hook_permission().
  */
diff --git a/core/modules/aggregator/aggregator.pages.inc b/core/modules/aggregator/aggregator.pages.inc
index b6d4062ec39ccb26ac6349d092f186473308f698..c5245059dbe7efaf094db4ee41612b86b0a6359f 100644
--- a/core/modules/aggregator/aggregator.pages.inc
+++ b/core/modules/aggregator/aggregator.pages.inc
@@ -318,41 +318,6 @@ function template_preprocess_aggregator_item(&$variables) {
   $variables['attributes']['class'][] = 'feed-item';
 }
 
-/**
- * Page callback: Displays all the categories used by the Aggregator module.
- *
- * @return string
- *   An HTML formatted string.
- *
- * @see aggregator_menu()
- */
-function aggregator_page_categories() {
-  $result = db_query('SELECT c.cid, c.title, c.description FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid LEFT JOIN {aggregator_item} i ON ci.iid = i.iid GROUP BY c.cid, c.title, c.description');
-
-  $build = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('aggregator-wrapper')),
-    '#sorted' => TRUE,
-  );
-  $aggregator_summary_items = config('aggregator.settings')->get('source.list_max');
-  foreach ($result as $category) {
-    $summary_items = array();
-    if ($aggregator_summary_items) {
-      if ($items = aggregator_load_feed_items('category', $category, $aggregator_summary_items)) {
-        $summary_items = entity_view_multiple($items, 'summary');
-      }
-    }
-    $category->url = url('aggregator/categories/' . $category->cid);
-    $build[$category->cid] = array(
-      '#theme' => 'aggregator_summary_items',
-      '#summary_items' => $summary_items,
-      '#source' => $category,
-    );
-  }
-
-  return $build;
-}
-
 /**
  * Page callback: Generates an RSS 0.92 feed of aggregator items or categories.
  *
diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml
index e25527c4c9d956d60e7f95e27b5b78d0f4cec2fe..2c69045b6a5fd3ee9928bad88129f5dd71993e47 100644
--- a/core/modules/aggregator/aggregator.routing.yml
+++ b/core/modules/aggregator/aggregator.routing.yml
@@ -60,3 +60,10 @@ aggregator_sources:
     _content: '\Drupal\aggregator\Controller\AggregatorController::sources'
   requirements:
     _permission: 'access news feeds'
+
+aggregator_categories:
+  pattern: '/aggregator/categories'
+  defaults:
+    _content: '\Drupal\aggregator\Controller\AggregatorController::categories'
+  requirements:
+    _access_aggregator_categories: 'TRUE'
diff --git a/core/modules/aggregator/aggregator.services.yml b/core/modules/aggregator/aggregator.services.yml
index 639ed2d5b0a410c9dba08c99d259b8b5f63031f4..cf74241729bf89bd9e0b91fb48b6767cc40cbe1d 100644
--- a/core/modules/aggregator/aggregator.services.yml
+++ b/core/modules/aggregator/aggregator.services.yml
@@ -8,3 +8,8 @@ services:
   plugin.manager.aggregator.processor:
     class: Drupal\aggregator\Plugin\AggregatorPluginManager
     arguments: [processor, '@container.namespaces']
+  access_check.aggregator.categories:
+    class: Drupal\aggregator\Access\CategoriesAccessCheck
+    arguments: ['@database']
+    tags:
+      - { name: access_check }
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Access/CategoriesAccessCheck.php b/core/modules/aggregator/lib/Drupal/aggregator/Access/CategoriesAccessCheck.php
new file mode 100644
index 0000000000000000000000000000000000000000..576495e47bc4dcaf0d0e63028ec35782032a7e1f
--- /dev/null
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Access/CategoriesAccessCheck.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\Access\CategoriesAccess.
+ */
+
+namespace Drupal\aggregator\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Drupal\Core\Database\Connection;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides an access check for aggregator categories routes.
+ */
+class CategoriesAccessCheck implements AccessCheckInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Constructs a CategoriesAccessCheck object.
+   *
+   * @param \Drupal\Core\Database\Connection
+   *   The database connection.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_access_aggregator_categories', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    // @todo Replace user_access() with a correctly injected and session-using
+    // alternative.
+    return user_access('access news feeds') && (bool) $this->database->queryRange('SELECT 1 FROM {aggregator_category}', 0, 1)->fetchField();
+  }
+
+}
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Controller/AggregatorController.php b/core/modules/aggregator/lib/Drupal/aggregator/Controller/AggregatorController.php
index eac277c8c0c3581ea674c9bd2bd057e7959fbaa4..3fd7daee32e3d37c473775406e38adf5c88c2ede 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Controller/AggregatorController.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Controller/AggregatorController.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityManager;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Routing\PathBasedGeneratorInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -51,6 +52,13 @@ class AggregatorController implements ControllerInterface {
    */
   protected $moduleHandler;
 
+  /**
+   * The url generator.
+   *
+   * @var \Drupal\Core\Routing\PathBasedGeneratorInterface
+   */
+  protected $urlGenerator;
+
   /**
    * Constructs a \Drupal\aggregator\Controller\AggregatorController object.
    *
@@ -65,11 +73,12 @@ class AggregatorController implements ControllerInterface {
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    */
-  public function __construct(EntityManager $entity_manager, Connection $database, ConfigFactory $config_factory, ModuleHandlerInterface $module_handler) {
+  public function __construct(EntityManager $entity_manager, Connection $database, ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, PathBasedGeneratorInterface $url_generator) {
     $this->entityManager = $entity_manager;
     $this->database = $database;
     $this->configFactory = $config_factory;
     $this->moduleHandler = $module_handler;
+    $this->urlGenerator = $url_generator;
   }
 
   /**
@@ -80,7 +89,8 @@ public static function create(ContainerInterface $container) {
       $container->get('plugin.manager.entity'),
       $container->get('database'),
       $container->get('config.factory'),
-      $container->get('module_handler')
+      $container->get('module_handler'),
+      $container->get('url_generator')
     );
   }
 
@@ -125,7 +135,7 @@ public function feedRefresh(FeedInterface $aggregator_feed, Request $request) {
 
     // @todo after https://drupal.org/node/1972246 find a new place for it.
     aggregator_refresh($aggregator_feed);
-    return new RedirectResponse(url('admin/config/services/aggregator', array('absolute' => TRUE)));
+    return new RedirectResponse($this->urlGenerator->generateFromPath('admin/config/services/aggregator', array('absolute' => TRUE)));
   }
 
   /**
@@ -176,7 +186,7 @@ public function adminOverview() {
       '#theme' => 'table',
       '#header' => $header,
       '#rows' => $rows,
-      '#empty' =>  t('No feeds available. <a href="@link">Add feed</a>.', array('@link' => url('admin/config/services/aggregator/add/feed'))),
+      '#empty' => t('No feeds available. <a href="@link">Add feed</a>.', array('@link' => $this->urlGenerator->generateFromPath('admin/config/services/aggregator/add/feed'))),
     );
 
     $result = $this->database->query('SELECT c.cid, c.title, COUNT(ci.iid) as items FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid GROUP BY c.cid, c.title ORDER BY title');
@@ -205,12 +215,47 @@ public function adminOverview() {
       '#theme' => 'table',
       '#header' => $header,
       '#rows' => $rows,
-      '#empty' =>  t('No categories available. <a href="@link">Add category</a>.', array('@link' => url('admin/config/services/aggregator/add/category'))),
+      '#empty' => t('No categories available. <a href="@link">Add category</a>.', array('@link' => $this->urlGenerator->generateFromPath('admin/config/services/aggregator/add/category'))),
     );
 
     return $build;
   }
 
+  /**
+   * Displays all the categories used by the Aggregator module.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function categories() {
+    // @todo Refactor this once all controller conversions are complete.
+    $this->moduleHandler->loadInclude('aggregator', 'inc', 'aggregator.pages');
+
+    $result = $this->database->query('SELECT c.cid, c.title, c.description FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid LEFT JOIN {aggregator_item} i ON ci.iid = i.iid GROUP BY c.cid, c.title, c.description');
+
+    $build = array(
+      '#type' => 'container',
+      '#attributes' => array('class' => array('aggregator-wrapper')),
+      '#sorted' => TRUE,
+    );
+    $aggregator_summary_items = $this->configFactory->get('aggregator.settings')->get('source.list_max');
+    foreach ($result as $category) {
+      $summary_items = array();
+      if ($aggregator_summary_items) {
+        if ($items = aggregator_load_feed_items('category', $category, $aggregator_summary_items)) {
+          $summary_items = $this->entityManager->getRenderController('aggregator_item')->viewMultiple($items, 'summary');
+        }
+      }
+      $category->url = $this->urlGenerator->generateFromPath('aggregator/categories/' . $category->cid);
+      $build[$category->cid] = array(
+        '#theme' => 'aggregator_summary_items',
+        '#summary_items' => $summary_items,
+        '#source' => $category,
+      );
+    }
+    return $build;
+  }
+
   /**
    * Displays the most recent items gathered from any feed.
    *
@@ -263,7 +308,7 @@ public function sources() {
             ->viewMultiple($items, 'summary');
         }
       }
-      $feed->url = url('aggregator/sources/' . $feed->id());
+      $feed->url = $this->urlGenerator->generateFromPath('aggregator/sources/' . $feed->id());
       $build[$feed->id()] = array(
         '#theme' => 'aggregator_summary_items',
         '#summary_items' => $summary_items,