diff --git a/.editorconfig b/.editorconfig
index 79207a40cb9326b8c6b8c958fa864b5345f94e68..0fcdf7fd4fc5705384ea40f93be21e0fc85ac557 100755
--- a/.editorconfig
+++ b/.editorconfig
@@ -17,6 +17,3 @@ indent_size = 4
 [{.jshintrc,*.json,*.yml}]
 indent_style = space
 indent_size = 2
-
-[{*.txt,wp-config-sample.php}]
-end_of_line = crlf
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 40c53b76bbfcdb3b614b4ff21a13d2a52de56d50..a4d480111b33701c1f5cd7785d3f506e883a7b21 100755
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ variables:
   # Configure mysql service (https://hub.docker.com/_/mysql/)
   MYSQL_DATABASE: wordpress_tests
   MYSQL_ROOT_PASSWORD: mysql
+  WP_VERSION: latest
 
 cache:
   paths:
@@ -10,7 +11,7 @@ cache:
 
 before_script:
   # Set up WordPress tests
-  - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql latest true
+  - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql $WP_VERSION true
 
   # PHPUnit
   - |
@@ -20,10 +21,43 @@ before_script:
       composer global require "phpunit/phpunit=4.8.*"
     fi
 
-  # Install PHPCS and WPCS
-  - composer global require automattic/vipwpcs
-  - composer global require phpcompatibility/phpcompatibility-wp
-  - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp
+PHPunit:PHP5.3:MySQL:
+  stage: test
+  variables:
+    WP_VERSION: 5.1.1
+  image: containers.ethitter.com:443/docker/images/php:5.3
+  services:
+    - mysql:5.6
+  script:
+    - find . -type "f" -iname "*.php" | xargs -L "1" php -l
+    - phpunit
+
+PHPunit:PHP5.6:MySQL:
+  stage: test
+  image: containers.ethitter.com:443/docker/images/php:5.6
+  services:
+    - mysql:5.6
+  script:
+    - find . -type "f" -iname "*.php" | xargs -L "1" php -l
+    - phpunit
+
+PHPunit:PHP7.0:MySQL:
+  stage: test
+  image: containers.ethitter.com:443/docker/images/php:7.0
+  services:
+    - mysql:5.6
+  script:
+    - find . -type "f" -iname "*.php" | xargs -L "1" php -l
+    - phpunit
+
+PHPunit:PHP7.1:MySQL:
+  stage: test
+  image: containers.ethitter.com:443/docker/images/php:7.1
+  services:
+    - mysql:5.6
+  script:
+    - find . -type "f" -iname "*.php" | xargs -L "1" php -l
+    - phpunit
 
 PHPunit:PHP7.2:MySQL:
   stage: test
@@ -32,9 +66,7 @@ PHPunit:PHP7.2:MySQL:
     - mysql:5.6
   script:
     - find . -type "f" -iname "*.php" | xargs -L "1" php -l
-    - phpcs -n
     - phpunit
-  allow_failure: true
 
 PHPunit:PHP7.3:MySQL:
   stage: test
@@ -43,13 +75,21 @@ PHPunit:PHP7.3:MySQL:
     - mysql:5.6
   script:
     - find . -type "f" -iname "*.php" | xargs -L "1" php -l
-    - phpcs -n
     - phpunit
-  allow_failure: true
+
+PHPCS:
+  stage: test
+  image: containers.ethitter.com:443/docker/images/php:7.3
+  before_script:
+    - composer global require automattic/vipwpcs
+    - composer global require phpcompatibility/phpcompatibility-wp
+    - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp
+  script:
+    - phpcs -n
 
 PluginSVN:
   stage: deploy
-  image: containers.ethitter.com:443/docker/images/php:7.3
+  image: containers.ethitter.com:443/docker/wp-org-plugin-deploy:latest
   before_script:
     - curl -o ./bin/deploy.sh https://git-cdn.e15r.co/open-source/wp-org-plugin-deploy/raw/master/scripts/deploy.sh
     - chmod +x ./bin/deploy.sh
diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index 7dfcb692f269238decac45ae18a1e6a510736005..6555ad3378423a8fc1a3f1466bb3ee1848986b61 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -26,7 +26,7 @@
 	<!-- Rules: WordPress Coding Standards -->
 	<!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards -->
 	<!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties -->
-	<config name="minimum_supported_wp_version" value="3.2.1"/>
+	<config name="minimum_supported_wp_version" value="4.4"/>
 	<rule ref="WordPress" />
 	<rule ref="WordPressVIPMinimum" />
 	<rule ref="WordPress-VIP-Go" />
diff --git a/README.md b/README.md
index a9166e400919ee66468366d3d72dda5e6a2bc899..e33e80de4a8e784357978575eb1830680acfb9c3 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 **Tags:** seo, meta tags  
 **Requires at least:** 4.4  
 **Tested up to:** 5.2  
-**Stable tag:** 0.5  
+**Stable tag:** 0.6  
 **License:** GPLv2 or later  
 **License URI:** http://www.gnu.org/licenses/gpl-2.0.html  
 
@@ -30,8 +30,25 @@ For example, http://example.com/?p=123 becomes http://example.com/p/123/.
 
 No, shortlinks use the posts' IDs, so aren't available for modification.
 
+### Why aren't redirects validated? ###
+
+Sites may use plugins that allow a post object's permalink to be set to an external URL, and this plugin is designed to respect those plugins.
+
+If you wish to validate the redirects issued by this plugin, you can use the `eth_simple_shortlinks_redirect_url` filter to apply `wp_validate_redirect()` to the destination URL.
+
+### After upgrading to 0.6, redirects stopped working ###
+
+Beginning with release 0.6, before performing a redirect, the plugin checks that the post type and post status are supported. Previously, these checks were only applied when overriding an object's shortlink.
+
+If, after upgrading, redirects stop working, use the `eth_simple_shortlinks_allowed_post_types` and `eth_simple_shortlinks_allowed_post_statuses` filters to permit additional types and statuses, or use the `eth_simple_shortlinks_verify_requested_post_support` filter to disable the supports checks.
+
 ## Changelog ##
 
+### 0.6 ###
+* Introduce filters in redirection handling.
+* Apply supported post-type and post-status checks before redirecting.
+* Conform to WordPress VIP's Coding Standards.
+
 ### 0.5 ###
 * Admin notices when permalinks won't support the plugin
 * Disable plugin functionality when permalink structure is incompatible
@@ -39,3 +56,9 @@ No, shortlinks use the posts' IDs, so aren't available for modification.
 
 ### 0.4 ###
 * Initial release
+
+## Upgrade Notice ##
+
+### 0.6 ###
+
+Applies supported post-type and post-status checks before performing redirect. If, after upgrading, redirects stop working, see the "After upgrading to 0.6, redirects stopped working" section of the FAQ.
diff --git a/eth-simple-shortlinks.php b/eth-simple-shortlinks.php
index 381f07d0915d3884d4b50ff4aca5348c4495181a..22eda06b43046b71a242adb3dd7f04bc731b1670 100644
--- a/eth-simple-shortlinks.php
+++ b/eth-simple-shortlinks.php
@@ -1,298 +1,33 @@
 <?php
-/*
-Plugin Name: ETH Simple Shortlinks
-Plugin URI: https://ethitter.com/plugins/
-Description: Simple non-GET shortlinks using post IDs
-Author: Erick Hitter
-Version: 0.5
-Author URI: https://ethitter.com/
-Text Domain: eth_simple_shortlinks
-Domain Path: /languages/
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-*/
-
-class ETH_Simple_Shortlinks {
-	/**
-	 * PLUGIN SETUP
-	 */
-
-	/**
-	 * Singleton
-	 */
-	private static $instance = null;
-
-	/**
-	 * Instantiate singleton
-	 */
-	public static function get_instance() {
-		if ( ! is_a( self::$instance, __CLASS__ ) ) {
-			self::$instance = new self;
-		}
-
-		return self::$instance;
-	}
-
-	/**
-	 * Dummy magic methods
-	 */
-	public function __clone() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; uh?', 'eth_simple_shortlinks' ), '0.1' ); }
-	public function __wakeup() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; uh?', 'eth_simple_shortlinks' ), '0.1' ); }
-	public function __call( $name = '', $args = array() ) { unset( $name, $args ); return null; }
-
-	/**
-	 * Class properties
-	 */
-	private $name          = 'ETH Simple Shortlinks';
-	private $slug          = 'p';
-	private $rewrite_rule  = null;
-	private $rewrite_match = null;
-	private $qv            = 'eth-shortlink';
-
-	private $plugin_supported = false;
-
-	private $supported_post_types    = array();
-	private $supported_post_statuses = array();
-
-	/**
-	 * Register plugin's setup action
-	 */
-	private function __construct() {
-		// Build rewrite parts using other class properties
-		$this->rewrite_rule  = '^' . $this->slug . '/([\d]+)/?$';
-		$this->rewrite_match = 'index.php?p=$matches[1]&' . $this->qv . '=1';
-
-		// Basic plugin actions
-		add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) );
-		add_action( 'init', array( $this, 'action_init' ) );
-	}
-
-	/**
-	 * Load plugin translations
-	 */
-	public function action_plugins_loaded() {
-		load_plugin_textdomain( 'eth_simple_shortlinks', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
-	}
-
-	/**
-	 * Verify plugin is supported, then register its functionality
-	 */
-	public function action_init() {
-		global $wp_rewrite;
-
-		// Plugin won't work if site doesn't use pretty permalinks
-		if ( empty( $wp_rewrite->permalink_structure ) ) {
-			add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) );
-		} else {
-			$this->plugin_supported = true;
-
-			// Admin notices
-			add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) );
-
-			// Register rewrite rule
-			add_rewrite_rule( $this->rewrite_rule, $this->rewrite_match, 'top' );
-
-			// Request handling
-			add_action( 'wp_loaded', array( $this, 'filter_support' ) );
-			add_filter( 'query_vars', array( $this, 'filter_query_vars' ) );
-			add_action( 'parse_request', array( $this, 'action_parse_request' ) );
-
-			// Shortlink overrides
-			add_filter( 'get_shortlink', array( $this, 'filter_get_shortlink' ), 10, 2 );
-			add_action( 'admin_head-edit.php', array( $this, 'add_admin_header_assets' ) );
-			add_filter( 'post_row_actions', array( $this, 'filter_row_actions' ), 10, 2 );
-			add_filter( 'page_row_actions', array( $this, 'filter_row_actions' ), 10, 2 );
-		}
-	}
-
-	/**
-	 * Display admin notices if plugin's requirements aren't met
-	 */
-	public function action_add_admin_notices() {
-		// Notices are only relevant if current user can get to the Permalinks and Plugins options screens
-		if ( ! current_user_can( 'manage_options') || ! current_user_can( 'activate_plugins' ) ) {
-			return;
-		}
-
-		// Build notices
-		$message = '';
-
-		if ( $this->plugin_supported ) {
-			// Check option for the plugin's rule
-			// The `$wp_rewrite` global will include it in `extra_rules_top` even though it hasn't been saved to the DB, and therefore isn't really active.
-			$rewrites = get_option( 'rewrite_rules' );
-
-			if ( is_array( $rewrites ) && ! array_key_exists( $this->rewrite_rule, $rewrites ) ) {
-				$message = sprintf( __( 'Please visit the <a href="%1$s">Permalinks</a> settings page to refresh your permalinks. Doing so will add the rules this plugin requires.', 'eth_simple_shortlinks' ), admin_url( 'options-permalink.php' ) );
-			}
-		} else {
-			$message = sprintf( __( 'Please enable <a href="%1$s">pretty permalinks</a>, otherwise disable this plugin as it is not compatible with "Plain" permalinks.', 'eth_simple_shortlinks' ), admin_url( 'options-permalink.php' ) );
-		}
-
-		// Display a notice if one exists
-		if ( ! empty( $message ) ) {
-			$message = sprintf( __( '<strong>%1$s</strong>: %2$s', 'eth_simple_shortlinks' ), $this->name, $message );
-
-			?><div class="error">
-				<p><?php echo $message; ?></p>
-			</div><?php
-		}
-	}
-
-	/**
-	 * PLUGIN FUNCTIONALITY
-	 */
-
-	/**
-	 * Allow filtering of supported statuses and types
-	 */
-	public function filter_support() {
-		$this->supported_post_statuses = apply_filters( 'eth_simple_shortlinks_allowed_post_statuses', array( 'publish', 'future' ) );
-		$this->supported_post_types    = apply_filters( 'eth_simple_shortlinks_allowed_post_types',    array( 'post', 'page' ) );
-	}
-
-	/**
-	 * Add custom query var to those permitted, so it can be detected at `parse_request`
-	 */
-	public function filter_query_vars( $qv ) {
-		$qv[] = $this->qv;
-
-		return $qv;
-	}
-
-	/**
-	 * Catch this plugin's requests and issue redirects, otherwise WP will serve content at duplicate URLs
-	 *
-	 * Allows invalid post IDs fall through to WP's 404 handler, or anything else that might intercede
-	 *
-	 * URLs aren't validated in case plugins filter permalinks to point to external URLs
-	 */
-	public function action_parse_request( $request ) {
-		if ( isset( $request->query_vars[ $this->qv ] ) ) {
-			$dest = get_permalink( $request->query_vars[ 'p' ] );
-
-			if ( $dest ) {
-				wp_redirect( $dest, 301 );
-				exit;
-			}
-		}
-	}
-
-	/**
-	 * Override shortlinks with this plugin's format
-	 */
-	public function filter_get_shortlink( $shortlink, $id ) {
-		if ( empty( $id ) ) {
-			$_p = get_post();
-
-			if ( ! empty( $_p->ID ) ) {
-				$id = $_p->ID;
-			}
-		}
-
-		if ( empty( $id ) ) {
-			return $shortlink;
-		}
-
-		if ( ! $this->is_supported_post_status( get_post_status( $id ) ) ) {
-			return $shortlink;
-		}
-
-		if ( ! $this->is_supported_post_type( get_post_type( $id ) ) ) {
-			return $shortlink;
-		}
-
-		return $this->get_shortlink( $id );
-	}
-
-	/**
-	 * Header assets for shortlink in row actions
-	 */
-	public function add_admin_header_assets() {
-		global $typenow;
-		if ( ! $this->is_supported_post_type( $typenow ) ) {
-			return;
-		}
-
-		?>
-		<script type="text/javascript">
-
-			jQuery( document ) .ready( function( $ ) {
-				$( '.row-actions .shortlink a' ).click( function( e ) {
-					e.preventDefault();
-
-					prompt( 'URL:', $( this ).attr('href') );
-				} );
-			} );
-		</script>
-		<?php
-	}
-
-	/**
-	 * Provide the shortlink in row actions for easy access
-	 */
-	public function filter_row_actions( $actions, $post ) {
-		if ( ! $this->is_supported_post_type( get_post_type( $post ) ) || ! $this->is_supported_post_status( get_post_status( $post ) ) ) {
-			return $actions;
-		}
-
-		$actions['shortlink'] = '<a href="' . esc_js( $this->get_shortlink( $post->ID ) ) . '">' . __( 'Shortlink', 'eth_simple_shortlinks' ) . '</a>';
-
-		return $actions;
-	}
-
-	/**
-	 * Check if given post type is supported
-	 */
-	private function is_supported_post_type( $type ) {
-		return in_array( $type, $this->supported_post_types );
-	}
-
-	/**
-	 * Check if given post status is supported
-	 */
-	private function is_supported_post_status( $status ) {
-		return in_array( $status, $this->supported_post_statuses );
-	}
-
-	/**
-	 * Utility method for building permlink
-	 */
-	public function get_shortlink( $post_id ) {
-		// Use Core's default when this plugin can't build a link
-		if ( ! $this->plugin_supported ) {
-			return wp_get_shortlink( $post_id );
-		}
-
-		return user_trailingslashit( home_url( sprintf( '%s/%d', $this->slug, $post_id ) ) );
-	}
-}
-
 /**
- * One instance to rule them all
+ * Load ETH Simple Shortlinks.
+ *
+ * @package ETH_Simple_Shortlinks
  */
-ETH_Simple_Shortlinks::get_instance();
 
 /**
- * Shortcut for using the shortlink outside of this plugin's considerations
+ * Plugin Name: ETH Simple Shortlinks
+ * Plugin URI: https://ethitter.com/plugins/
+ * Description: Simple non-GET shortlinks using post IDs
+ * Author: Erick Hitter
+ * Version: 0.6
+ * Author URI: https://ethitter.com/
+ * Text Domain: eth_simple_shortlinks
+ * Domain Path: /languages/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
-function eth_simple_shortlinks_get( $post_id ) {
-	if ( ! did_action( 'wp_loaded' ) ) {
-		_doing_it_wrong( __FUNCTION__, __( 'Shortlinks cannot be generated until after <code>wp_loaded</code>; this ensures that all post types are registered.', 'eth_simple_shortlinks' ), '0.3' );
-		return false;
-	}
 
-	return ETH_Simple_Shortlinks::get_instance()->get_shortlink( $post_id );
-}
+require_once __DIR__ . '/inc/class-eth-simple-shortlinks.php';
diff --git a/inc/class-eth-simple-shortlinks.php b/inc/class-eth-simple-shortlinks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9dbb12da2a015947d3aa4af4e06aa31ff3afa539
--- /dev/null
+++ b/inc/class-eth-simple-shortlinks.php
@@ -0,0 +1,491 @@
+<?php
+/**
+ * Plugin functionality.
+ *
+ * @package ETH_Simple_Shortlinks
+ */
+
+/**
+ * Class ETH_Simple_Shortlinks.
+ */
+class ETH_Simple_Shortlinks {
+	/**
+	 * PLUGIN SETUP
+	 */
+
+	/**
+	 * Singleton
+	 *
+	 * @var self
+	 */
+	private static $instance;
+
+	/**
+	 * Instantiate singleton
+	 */
+	public static function get_instance() {
+		if ( ! is_a( self::$instance, __CLASS__ ) ) {
+			self::$instance = new self();
+		}
+
+		return self::$instance;
+	}
+
+	/**
+	 * Dummy magic method
+	 */
+	public function __clone() {
+		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; uh?', 'eth_simple_shortlinks' ), '0.1' );
+	}
+
+	/**
+	 * Dummy magic method
+	 */
+	public function __wakeup() {
+		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; uh?', 'eth_simple_shortlinks' ), '0.1' );
+	}
+
+	/**
+	 * Dummy magic method
+	 *
+	 * @param string $name Method name.
+	 * @param array  $args Method arguments.
+	 * @return null
+	 */
+	public function __call( $name = '', $args = array() ) {
+		return null;
+	}
+
+	/**
+	 * Plugin name.
+	 *
+	 * @var string
+	 */
+	private $name = 'ETH Simple Shortlinks';
+
+	/**
+	 * Rewrite slug.
+	 *
+	 * @var string
+	 */
+	private $slug = 'p';
+
+	/**
+	 * Rewrite rule.
+	 *
+	 * @var string
+	 */
+	private $rewrite_rule;
+
+	/**
+	 * Rewrite rule match.
+	 *
+	 * @var string
+	 */
+	private $rewrite_match;
+
+	/**
+	 * Query variable.
+	 *
+	 * @var string
+	 */
+	private $qv = 'eth-shortlink';
+
+	/**
+	 * Is environment ready?
+	 *
+	 * @var bool
+	 */
+	private $plugin_supported = false;
+
+	/**
+	 * Post types supported by the plugin.
+	 *
+	 * @var array
+	 */
+	private $supported_post_types = array();
+
+	/**
+	 * Post statuses allowed to redirect to.
+	 *
+	 * @var array
+	 */
+	private $supported_post_statuses = array();
+
+	/**
+	 * Register plugin's setup action.
+	 */
+	private function __construct() {
+		// Build rewrite parts using other class properties.
+		$this->rewrite_rule  = '^' . $this->slug . '/([\d]+)/?$';
+		$this->rewrite_match = 'index.php?p=$matches[1]&' . $this->qv . '=1';
+
+		// Basic plugin actions.
+		add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) );
+		add_action( 'init', array( $this, 'action_init' ) );
+	}
+
+	/**
+	 * Load plugin translations.
+	 */
+	public function action_plugins_loaded() {
+		load_plugin_textdomain(
+			'eth_simple_shortlinks',
+			false,
+			dirname( plugin_basename( __FILE__ ) ) . '/languages/'
+		);
+	}
+
+	/**
+	 * Verify plugin is supported, then register its functionality.
+	 */
+	public function action_init() {
+		global $wp_rewrite;
+
+		// Plugin won't work if site doesn't use pretty permalinks.
+		if ( empty( $wp_rewrite->permalink_structure ) ) {
+			add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) );
+			return;
+		}
+
+		$this->plugin_supported = true;
+
+		// Admin notices.
+		add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) );
+
+		// Register rewrite rule.
+		add_rewrite_rule( $this->rewrite_rule, $this->rewrite_match, 'top' );
+
+		// Request handling.
+		add_action( 'wp_loaded', array( $this, 'filter_support' ) );
+		add_filter( 'query_vars', array( $this, 'filter_query_vars' ) );
+		add_action( 'parse_request', array( $this, 'action_parse_request' ) );
+
+		// Shortlink overrides.
+		add_filter( 'get_shortlink', array( $this, 'filter_get_shortlink' ), 10, 2 );
+		add_action( 'admin_head-edit.php', array( $this, 'add_admin_header_assets' ) );
+		add_filter( 'post_row_actions', array( $this, 'filter_row_actions' ), 10, 2 );
+		add_filter( 'page_row_actions', array( $this, 'filter_row_actions' ), 10, 2 );
+	}
+
+	/**
+	 * Display admin notices if plugin's requirements aren't met.
+	 */
+	public function action_add_admin_notices() {
+		// Notices are only relevant if current user can get to the Permalinks and Plugins options screens.
+		if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'activate_plugins' ) ) {
+			return;
+		}
+
+		// Build notices.
+		$message = '';
+
+		if ( $this->plugin_supported ) {
+			// Check option for the plugin's rule.
+			// The `$wp_rewrite` global will include it in `extra_rules_top` even though it hasn't been saved to the DB, and therefore isn't really active.
+			$rewrites = get_option( 'rewrite_rules' );
+
+			if ( is_array( $rewrites ) && ! array_key_exists( $this->rewrite_rule, $rewrites ) ) {
+				$message = sprintf(
+					/* translators: 1: URL of permalink options page. */
+					__(
+						'Please visit the <a href="%1$s">Permalinks</a> settings page to refresh your permalinks. Doing so will add the rules this plugin requires.',
+						'eth_simple_shortlinks'
+					),
+					admin_url( 'options-permalink.php' )
+				);
+			}
+		} else {
+			$message = sprintf(
+				/* translators: 1: URL of permalink options page. */
+				__(
+					'Please enable <a href="%1$s">pretty permalinks</a>, otherwise disable this plugin as it is not compatible with "Plain" permalinks.',
+					'eth_simple_shortlinks'
+				),
+				admin_url( 'options-permalink.php' )
+			);
+		}
+
+		// Display a notice if one exists.
+		if ( ! empty( $message ) ) {
+			$message = sprintf(
+				/* translators: 1: Plugin name, 2: Notice text. */
+				__(
+					'<strong>%1$s</strong>: %2$s',
+					'eth_simple_shortlinks'
+				),
+				$this->name,
+				$message
+			);
+
+			?>
+			<div class="error">
+				<p><?php echo wp_kses_post( $message ); ?></p>
+			</div>
+			<?php
+		}
+	}
+
+	/**
+	 * PLUGIN FUNCTIONALITY
+	 */
+
+	/**
+	 * Allow filtering of supported statuses and types.
+	 */
+	public function filter_support() {
+		/**
+		 * Filters supported post statuses.
+		 *
+		 * @param array $statuses Supported statuses.
+		 */
+		$this->supported_post_statuses = apply_filters( 'eth_simple_shortlinks_allowed_post_statuses', array( 'publish', 'future' ) );
+
+		/**
+		 * Filters supported post types.
+		 *
+		 * @param array $types Post types.
+		 */
+		$this->supported_post_types = apply_filters( 'eth_simple_shortlinks_allowed_post_types', array( 'post', 'page' ) );
+	}
+
+	/**
+	 * Add custom query var to those permitted, so it can be detected at `parse_request`.
+	 *
+	 * @param array $qv Registered query vars.
+	 * @return array
+	 */
+	public function filter_query_vars( $qv ) {
+		$qv[] = $this->qv;
+
+		return $qv;
+	}
+
+	/**
+	 * Catch this plugin's requests and issue redirects, otherwise WP
+	 * will serve content at duplicate URLs.
+	 *
+	 * Allows invalid post IDs fall through to WP's 404 handler, or
+	 * anything else that might intercede.
+	 *
+	 * @param WP $request WP object.
+	 */
+	public function action_parse_request( $request ) {
+		$redirect = $this->get_redirect_for_request( $request );
+
+		if ( null === $redirect ) {
+			return;
+		}
+
+		/**
+		 * URLs aren't validated in case plugins filter permalinks to point to external URLs.
+		 *
+		 * If validation is desired, hook into the `eth_simple_shortlinks_redirect_url` filter.
+		 */
+		// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
+		wp_redirect( $redirect->destination, $redirect->status_code );
+		exit;
+	}
+
+	/**
+	 * Parse a given WP object for its redirect destination.
+	 *
+	 * @param WP $request WP object.
+	 * @return \stdClass|null
+	 */
+	public function get_redirect_for_request( $request ) {
+		if ( ! isset( $request->query_vars[ $this->qv ], $request->query_vars['p'] ) ) {
+			return null;
+		}
+
+		$post_object = get_post( $request->query_vars['p'] );
+
+		if ( ! $post_object instanceof WP_Post ) {
+			return null;
+		}
+
+		/**
+		 * Filters if post type and status should be validated.
+		 *
+		 * @since 0.6
+		 *
+		 * @param bool     $validate   Perform validation.
+		 * @param WP_Post $post_object Post being redirected to.
+		 * @param WP      $request     WP object.
+		 */
+		if (
+			apply_filters( 'eth_simple_shortlinks_verify_requested_post_support', true, $post_object, $request ) &&
+			(
+				! $this->is_supported_post_type( $post_object->post_type ) ||
+				! $this->is_supported_post_status( $post_object->post_status )
+			)
+		) {
+			return null;
+		}
+
+		/**
+		 * Filters the redirect URL.
+		 *
+		 * @since 0.6
+		 *
+		 * @param string  $destination Redirect destination.
+		 * @param WP_Post $post_object Post being redirected to.
+		 * @param WP      $request     WP object.
+		 */
+		$destination = apply_filters( 'eth_simple_shortlinks_redirect_url', get_permalink( $post_object ), $post_object, $request );
+
+		/**
+		 * Filters the redirect status code.
+		 *
+		 * @since 0.6
+		 *
+		 * @param int     $status_code Redirect status code.
+		 * @param string  $destination Redirect destination.
+		 * @param WP_Post $post_object Post being redirected to.
+		 * @param WP      $request     WP object.
+		 */
+		$status_code = (int) apply_filters( 'eth_simple_shortlinks_redirect_status', 301, $destination, $post_object, $request );
+
+		if ( empty( $destination ) || empty( $status_code ) ) {
+			return null;
+		}
+
+		return (object) compact( 'destination', 'status_code' );
+	}
+
+	/**
+	 * Override shortlinks with this plugin's format.
+	 *
+	 * @param string $shortlink Short URL.
+	 * @param int    $id        Post ID.
+	 * @return string
+	 */
+	public function filter_get_shortlink( $shortlink, $id ) {
+		if ( empty( $id ) ) {
+			$_p = get_post();
+
+			if ( ! empty( $_p->ID ) ) {
+				$id = $_p->ID;
+			}
+		}
+
+		if ( empty( $id ) ) {
+			return $shortlink;
+		}
+
+		if ( ! $this->is_supported_post_status( get_post_status( $id ) ) ) {
+			return $shortlink;
+		}
+
+		if ( ! $this->is_supported_post_type( get_post_type( $id ) ) ) {
+			return $shortlink;
+		}
+
+		return $this->get_shortlink( $id );
+	}
+
+	/**
+	 * Header assets for shortlink in row actions.
+	 */
+	public function add_admin_header_assets() {
+		global $typenow;
+		if ( ! $this->is_supported_post_type( $typenow ) ) {
+			return;
+		}
+
+		?>
+		<script type="text/javascript">
+
+			jQuery( document ) .ready( function( $ ) {
+				$( '.row-actions .shortlink a' ).click( function( e ) {
+					e.preventDefault();
+
+					prompt( 'URL:', $( this ).attr('href') );
+				} );
+			} );
+		</script>
+		<?php
+	}
+
+	/**
+	 * Provide the shortlink in row actions for easy access.
+	 *
+	 * @param array   $actions Row actions.
+	 * @param WP_Post $post Post object.
+	 * @return array
+	 */
+	public function filter_row_actions( $actions, $post ) {
+		if ( ! $this->is_supported_post_type( get_post_type( $post ) ) || ! $this->is_supported_post_status( get_post_status( $post ) ) ) {
+			return $actions;
+		}
+
+		$actions['shortlink'] = '<a href="' . esc_js( $this->get_shortlink( $post->ID ) ) . '">' . __( 'Shortlink', 'eth_simple_shortlinks' ) . '</a>';
+
+		return $actions;
+	}
+
+	/**
+	 * Check if given post type is supported.
+	 *
+	 * @param string $type Post type.
+	 * @return bool
+	 */
+	private function is_supported_post_type( $type ) {
+		return in_array( $type, $this->supported_post_types, true );
+	}
+
+	/**
+	 * Check if given post status is supported.
+	 *
+	 * @param string $status Post status.
+	 * @return bool
+	 */
+	private function is_supported_post_status( $status ) {
+		return in_array( $status, $this->supported_post_statuses, true );
+	}
+
+	/**
+	 * Utility method for building permalink.
+	 *
+	 * @param int $post_id Post ID.
+	 * @return string
+	 */
+	public function get_shortlink( $post_id ) {
+		// Use Core's default when this plugin can't build a link.
+		if ( ! $this->plugin_supported ) {
+			return wp_get_shortlink( $post_id );
+		}
+
+		return user_trailingslashit( home_url( sprintf( '%s/%d', $this->slug, $post_id ) ) );
+	}
+}
+
+/**
+ * One instance to rule them all
+ */
+ETH_Simple_Shortlinks::get_instance();
+
+/**
+ * Shortcut for using the shortlink outside of this plugin's considerations.
+ *
+ * @param int $post_id Post ID.
+ * @return string
+ */
+function eth_simple_shortlinks_get( $post_id ) {
+	if ( ! did_action( 'wp_loaded' ) ) {
+		_doing_it_wrong(
+			__FUNCTION__,
+			wp_kses_post(
+				__(
+					'Shortlinks cannot be generated until after <code>wp_loaded</code>; this ensures that all post types are registered.',
+					'eth_simple_shortlinks'
+				)
+			),
+			'0.3'
+		);
+
+		return false;
+	}
+
+	return ETH_Simple_Shortlinks::get_instance()->get_shortlink( $post_id );
+}
diff --git a/languages/eth-simple-shortlinks.pot b/languages/eth-simple-shortlinks.pot
index bf9b3a5f7cec79430b85db643333b98b4e8b850a..a5b77c8aff7dbd20fdc0b8ef7b0e7c6b142710bb 100644
--- a/languages/eth-simple-shortlinks.pot
+++ b/languages/eth-simple-shortlinks.pot
@@ -2,10 +2,10 @@
 # This file is distributed under the same license as the ETH Simple Shortlinks package.
 msgid ""
 msgstr ""
-"Project-Id-Version: ETH Simple Shortlinks 0.5\n"
+"Project-Id-Version: ETH Simple Shortlinks 0.6\n"
 "Report-Msgid-Bugs-To: "
 "https://wordpress.org/support/plugin/eth-simple-shortlinks\n"
-"POT-Creation-Date: 2019-04-14 04:31:11+00:00\n"
+"POT-Creation-Date: 2019-05-12 23:21:03+00:00\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -25,31 +25,35 @@ msgstr ""
 "X-Poedit-Bookmarks: \n"
 "X-Textdomain-Support: yes\n"
 
-#: eth-simple-shortlinks.php:51 eth-simple-shortlinks.php:52
+#: inc/class-eth-simple-shortlinks.php:38
+#: inc/class-eth-simple-shortlinks.php:45
 msgid "Cheatin&#8217; uh?"
 msgstr ""
 
-#: eth-simple-shortlinks.php:138
+#: inc/class-eth-simple-shortlinks.php:191
+#. translators: 1: URL of permalink options page.
 msgid ""
 "Please visit the <a href=\"%1$s\">Permalinks</a> settings page to refresh "
 "your permalinks. Doing so will add the rules this plugin requires."
 msgstr ""
 
-#: eth-simple-shortlinks.php:141
+#: inc/class-eth-simple-shortlinks.php:201
+#. translators: 1: URL of permalink options page.
 msgid ""
 "Please enable <a href=\"%1$s\">pretty permalinks</a>, otherwise disable "
 "this plugin as it is not compatible with \"Plain\" permalinks."
 msgstr ""
 
-#: eth-simple-shortlinks.php:146
+#: inc/class-eth-simple-shortlinks.php:213
+#. translators: 1: Plugin name, 2: Notice text.
 msgid "<strong>%1$s</strong>: %2$s"
 msgstr ""
 
-#: eth-simple-shortlinks.php:251
+#: inc/class-eth-simple-shortlinks.php:422
 msgid "Shortlink"
 msgstr ""
 
-#: eth-simple-shortlinks.php:293
+#: inc/class-eth-simple-shortlinks.php:479
 msgid ""
 "Shortlinks cannot be generated until after <code>wp_loaded</code>; this "
 "ensures that all post types are registered."
diff --git a/readme.txt b/readme.txt
index 5c47f052e0e907f0fb4f4d5fcd4156b2397399d0..42ca0fee77b1857e91599de6ecc5286445d15185 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Donate link: https://ethitter.com/donate/
 Tags: seo, meta tags
 Requires at least: 4.4
 Tested up to: 5.2
-Stable tag: 0.5
+Stable tag: 0.6
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -30,8 +30,25 @@ For example, http://example.com/?p=123 becomes http://example.com/p/123/.
 
 No, shortlinks use the posts' IDs, so aren't available for modification.
 
+= Why aren't redirects validated? =
+
+Sites may use plugins that allow a post object's permalink to be set to an external URL, and this plugin is designed to respect those plugins.
+
+If you wish to validate the redirects issued by this plugin, you can use the `eth_simple_shortlinks_redirect_url` filter to apply `wp_validate_redirect()` to the destination URL.
+
+= After upgrading to 0.6, redirects stopped working =
+
+Beginning with release 0.6, before performing a redirect, the plugin checks that the post type and post status are supported. Previously, these checks were only applied when overriding an object's shortlink.
+
+If, after upgrading, redirects stop working, use the `eth_simple_shortlinks_allowed_post_types` and `eth_simple_shortlinks_allowed_post_statuses` filters to permit additional types and statuses, or use the `eth_simple_shortlinks_verify_requested_post_support` filter to disable the supports checks.
+
 == Changelog ==
 
+= 0.6 =
+* Introduce filters in redirection handling.
+* Apply supported post-type and post-status checks before redirecting.
+* Conform to WordPress VIP's Coding Standards.
+
 = 0.5 =
 * Admin notices when permalinks won't support the plugin
 * Disable plugin functionality when permalink structure is incompatible
@@ -39,3 +56,9 @@ No, shortlinks use the posts' IDs, so aren't available for modification.
 
 = 0.4 =
 * Initial release
+
+== Upgrade Notice ==
+
+= 0.6 =
+
+Applies supported post-type and post-status checks before performing redirect. If, after upgrading, redirects stop working, see the "After upgrading to 0.6, redirects stopped working" section of the FAQ.
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index f40e77271f4e5330b21b07efd0341ac3fb7abf57..64e9b989474adbc19f6ff29207308a53fa076f5a 100755
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -23,6 +23,9 @@ require_once $_tests_dir . '/includes/functions.php';
  * Manually load the plugin being tested.
  */
 function _manually_load_plugin() {
+	// Plugin requires a permalink structure to operate.
+	_set_default_permalink_structure_for_tests();
+
 	require dirname( dirname( __FILE__ ) ) . '/eth-simple-shortlinks.php';
 }
 tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
diff --git a/tests/test-plugin.php b/tests/test-plugin.php
new file mode 100755
index 0000000000000000000000000000000000000000..1252510bfd9fc6b468c0a92554b6a0aa6f12d750
--- /dev/null
+++ b/tests/test-plugin.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Class PluginTest
+ *
+ * @package ETH_Simple_Shortlinks
+ */
+
+/**
+ * Plugin test case.
+ */
+class PluginTest extends WP_UnitTestCase {
+	/**
+	 * Post ID for published tests.
+	 *
+	 * @var int
+	 */
+	protected static $post_id_published;
+
+	/**
+	 * Post ID for draft tests.
+	 *
+	 * @var int
+	 */
+	protected static $post_id_draft;
+
+	/**
+	 * Create a post to test with.
+	 */
+	public function setUp() {
+		parent::setUp();
+
+		static::$post_id_published = $this->factory->post->create();
+		static::$post_id_draft     = $this->factory->post->create(
+			array(
+				'post_status' => 'draft',
+			)
+		);
+	}
+
+	/**
+	 * Test shortlink overrides.
+	 */
+	public function test_shortlink_filters() {
+		$expected_published = user_trailingslashit( home_url( 'p/' . static::$post_id_published ) );
+		$expected_draft     = add_query_arg( 'p', static::$post_id_draft, user_trailingslashit( home_url() ) );
+
+		$this->assertEquals( $expected_published, wp_get_shortlink( static::$post_id_published ), 'Failed to assert that published post has a simple shortlink.' );
+		$this->assertEquals( $expected_draft, wp_get_shortlink( static::$post_id_draft ), 'Failed to assert that draft post did not have its shortlink modified.' );
+	}
+
+	/**
+	 * Test redirect parsing for supported post.
+	 */
+	public function test_published_post_redirect() {
+		$fake_request = new \stdClass();
+		$fake_request->query_vars = array(
+			'p'             => static::$post_id_published,
+			'eth-shortlink' => true,
+		);
+
+		$redirect = ETH_Simple_Shortlinks::get_instance()->get_redirect_for_request( $fake_request );
+
+		$this->assertEquals( get_permalink( static::$post_id_published ), $redirect->destination, 'Failed to assert that redirect destination is post\'s permalink.' );
+		$this->assertEquals( 301, $redirect->status_code, 'Failed to assert that redirect status code is that for a permanent redirect.' );
+	}
+
+	/**
+	 * Test redirect parsing for unsupported post.
+	 */
+	public function test_draft_post_redirect() {
+		$fake_request = new \stdClass();
+		$fake_request->query_vars = array(
+			'p'             => static::$post_id_draft,
+			'eth-shortlink' => true,
+		);
+
+		$redirect = ETH_Simple_Shortlinks::get_instance()->get_redirect_for_request( $fake_request );
+
+		$this->assertNull( $redirect, 'Failed to assert that redirect is not generated for unsupported post status.' );
+	}
+}
diff --git a/tests/test-sample.php b/tests/test-sample.php
deleted file mode 100755
index d2da28996e7c1ae676df62c9fd21fc2459aa7e15..0000000000000000000000000000000000000000
--- a/tests/test-sample.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-/**
- * Class SampleTest
- *
- * @package ETH_Simple_Shortlinks
- */
-
-/**
- * Sample test case.
- */
-class SampleTest extends WP_UnitTestCase {
-
-	/**
-	 * A single example test.
-	 */
-	public function test_sample() {
-		// Replace this with some actual testing code.
-		$this->assertTrue( true );
-	}
-}