diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 57ff8c2..51aabab 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -107,18 +107,12 @@ function filter_element_info() {
 function filter_menu() {
   $items['filter/tips'] = array(
     'title' => 'Compose tips',
-    'page callback' => 'filter_tips_long',
-    'access callback' => TRUE,
     'type' => MENU_SUGGESTED_ITEM,
-    'file' => 'filter.pages.inc',
+    'route_name' => 'filter_tips_all',
   );
   $items['filter/tips/%filter_format'] = array(
     'title' => 'Compose tips',
-    'page callback' => 'filter_tips_long',
-    'page arguments' => array(2),
-    'access callback' => 'filter_access',
-    'access arguments' => array(2),
-    'file' => 'filter.pages.inc',
+    'route_name' => 'filter_tips',
   );
   $items['admin/config/content/formats'] = array(
     'title' => 'Text formats',
diff --git a/core/modules/filter/filter.pages.inc b/core/modules/filter/filter.pages.inc
index 5b20d4f..8f2e298 100644
--- a/core/modules/filter/filter.pages.inc
+++ b/core/modules/filter/filter.pages.inc
@@ -6,28 +6,6 @@
  */
 
 /**
- * Page callback: Displays a page with long filter tips.
- *
- * @param $format
- *   (optional) A filter format. Defaults to NULL.
- *
- * @return string
- *   An HTML-formatted string.
- *
- * @see filter_menu()
- * @see theme_filter_tips()
- */
-function filter_tips_long($format = NULL) {
-  if (!empty($format)) {
-    $output = theme('filter_tips', array('tips' => _filter_tips($format->format, TRUE), 'long' => TRUE));
-  }
-  else {
-    $output = theme('filter_tips', array('tips' => _filter_tips(-1, TRUE), 'long' => TRUE));
-  }
-  return $output;
-}
-
-/**
  * Returns HTML for a set of filter tips.
  *
  * @param array $variables
diff --git a/core/modules/filter/filter.routing.yml b/core/modules/filter/filter.routing.yml
index de9e9cb..a8f4186 100644
--- a/core/modules/filter/filter.routing.yml
+++ b/core/modules/filter/filter.routing.yml
@@ -4,3 +4,17 @@ filter_admin_disable:
     _form: '\Drupal\filter\Form\DisableForm'
   requirements:
     _filter_disable_format_access: 'TRUE'
+
+filter_tips_all:
+  pattern: '/filter/tips'
+  defaults:
+    _content: '\Drupal\filter\Controller\FilterController::filterTips'
+  requirements:
+    _access: 'TRUE'
+
+filter_tips:
+  pattern: '/filter/tips/{filter_format}'
+  defaults:
+    _content: '\Drupal\filter\Controller\FilterController::filterTips'
+  requirements:
+    _filter_access: 'TRUE'
diff --git a/core/modules/filter/filter.services.yml b/core/modules/filter/filter.services.yml
index af11a10..7c85e37 100644
--- a/core/modules/filter/filter.services.yml
+++ b/core/modules/filter/filter.services.yml
@@ -10,3 +10,7 @@ services:
     class: Drupal\filter\Access\FormatDisableCheck
     tags:
       - { name: access_check }
+  access_check.filter_access:
+    class: Drupal\filter\Access\FilterAccess
+    tags:
+      - { name: access_check }
diff --git a/core/modules/filter/lib/Drupal/filter/Access/FilterAccess.php b/core/modules/filter/lib/Drupal/filter/Access/FilterAccess.php
new file mode 100644
index 0000000..7a63c50
--- /dev/null
+++ b/core/modules/filter/lib/Drupal/filter/Access/FilterAccess.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Access\FilterAccess.
+ */
+
+namespace Drupal\filter\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Checks access for text formats.
+ */
+class FilterAccess implements AccessCheckInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_filter_access', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    if ($format = $request->attributes->get('filter_format')) {
+      // Handle special cases up front. All users have access to the fallback
+      // format.
+      if ($format->format == filter_fallback_format()) {
+        return TRUE;
+      }
+
+      // Check the permission if one exists; otherwise, we have a non-existent
+      // format so we return FALSE.
+      $permission = filter_permission_name($format);
+      return !empty($permission) && user_access($permission);
+    }
+  }
+}
diff --git a/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
new file mode 100644
index 0000000..ca4e733
--- /dev/null
+++ b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Controller\FilterController.
+ */
+
+namespace Drupal\filter\Controller;
+
+use Drupal\filter\FilterFormatInterface;
+
+/**
+ * Controller routines for filter routes.
+ */
+class FilterController {
+
+  /**
+   * Displays a page with long filter tips.
+   *
+   * @param \Drupal\filter\FilterFormatInterface|null $format
+   *   A filter format, or NULL to show tips for all formats. Defaults to NULL.
+   *
+   * @return array
+   *   A renderable array.
+   *
+   * @see theme_filter_tips()
+   */
+  function filterTips(FilterFormatInterface $filter_format = NULL) {
+    $tips = $filter_format ? $filter_format->format : -1;
+
+    $build = array(
+      '#theme' => 'filter_tips',
+      '#long' => TRUE,
+      '#tips' => _filter_tips($tips, TRUE),
+    );
+
+    return $build;
+  }
+
+}
