diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 73da1815ea08528e216196746a390d7132a9e129..b44537577a8e94c6de0a4df2a5f46957f6cfc335 100755
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,2 +1,13 @@
 include:
   - remote: https://git-cdn.e15r.co/gitlab/ci/wordpress/-/raw/main/plugins/default.yml
+
+# Plugin does not support 5.6 or 7.0.
+PHPunit:PHP5.6:MySQL:
+  rules:
+    - if: $PIPELINE_PHP_5_6 != '1'
+      when: never
+
+PHPunit:PHP7.0:MySQL:
+  rules:
+    - if: $PIPELINE_PHP_7_0 != '1'
+      when: never
diff --git a/eth-embed-anchor-fm.php b/eth-embed-anchor-fm.php
index bb370d507efeffd83971d6bc716855fc762b5ee2..89de486694b5dcff420244f8fceb5b279f0bb01d 100644
--- a/eth-embed-anchor-fm.php
+++ b/eth-embed-anchor-fm.php
@@ -27,3 +27,23 @@
  */
 
 namespace ETH_Embed_Anchor_FM;
+
+/**
+ * Perform setup actions after plugin loads.
+ *
+ * @return void
+ */
+function action_plugins_loaded() {
+	load_plugin_textdomain(
+		'eth-embed-anchor-fm',
+		false,
+		dirname( plugin_basename( __FILE__ ) ) . '/languages/'
+	);
+}
+add_action( 'plugins_loaded', __NAMESPACE__ . '\action_plugins_loaded' );
+
+/**
+ * Load plugin classes.
+ */
+require_once __DIR__ . '/inc/class-plugin.php';
+Plugin::get_instance();
diff --git a/inc/class-plugin.php b/inc/class-plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef8c6d6548d6ed47e7b7747af9934468b09e8329
--- /dev/null
+++ b/inc/class-plugin.php
@@ -0,0 +1,186 @@
+<?php
+/**
+ * Plugin functionality.
+ *
+ * @package ETH_Embed_Anchor_FM
+ */
+
+namespace ETH_Embed_Anchor_FM;
+
+/**
+ * Class Plugin.
+ */
+class Plugin {
+	/**
+	 * Regex pattern to match URL to be oEmbedded.
+	 *
+	 * @var string
+	 */
+	private const OEMBED_FORMAT = '#^https://anchor\.fm/(?!api)([^/]+)/episodes/([^/\s]+)/?#i';
+
+	/**
+	 * Anchor oEmbed endpoint with placeholder.
+	 *
+	 * @var string
+	 */
+	private const OEMBED_ENDPOINT = 'https://anchor.fm/api/v3/episodes/__EPISODE_ID__/oembed';
+
+	/**
+	 * Placeholder in self::OEMBED_ENDPOINT to be replaced with episode ID.
+	 *
+	 * @var string
+	 */
+	private const EPISODE_ID_PLACEHOLDER = '__EPISODE_ID__';
+
+	/**
+	 * Shortcode tag.
+	 *
+	 * @var string
+	 */
+	private const SHORTCODE_TAG = 'eth_anchor_fm';
+
+	/**
+	 * Singleton.
+	 *
+	 * @var Plugin
+	 */
+	private static $_instance = null;
+
+	/**
+	 * Implement singleton.
+	 *
+	 * @return Plugin
+	 */
+	public static function get_instance(): Plugin {
+		if ( ! is_a( self::$_instance, __CLASS__ ) ) {
+			self::$_instance = new self();
+			self::$_instance->_setup();
+		}
+
+		return self::$_instance;
+	}
+
+	/**
+	 * Silence is golden!
+	 */
+	private function __construct() {
+		// Add nothing here.
+	}
+
+	/**
+	 * Register hooks.
+	 *
+	 * @return void
+	 */
+	private function _setup(): void {
+		add_action( 'init', [ $this, 'action_init' ] );
+
+		add_filter(
+			'oembed_fetch_url',
+			[ $this, 'filter_oembed_fetch_url' ],
+			10,
+			3
+		);
+	}
+
+	/**
+	 * Register oEmbed handler.
+	 *
+	 * @return void
+	 */
+	public function action_init(): void {
+		wp_oembed_add_provider(
+			self::OEMBED_FORMAT,
+			self::OEMBED_ENDPOINT,
+			true
+		);
+
+		add_shortcode(
+			self::SHORTCODE_TAG,
+			[ $this, 'do_shortcode' ]
+		);
+	}
+
+	/**
+	 * Filter oEmbed URL.
+	 *
+	 * Anchor.fm's oEmbed endpoint is specific to an episode ID, which must be
+	 * extracted from the episode URL.
+	 *
+	 * @param string $provider URL of the oEmbed provider.
+	 * @param string $url      URL of the content to be embedded.
+	 * @param array  $args     Optional. Additional arguments for retrieving
+	 *                         embed HTML.
+	 * @return string
+	 */
+	public function filter_oembed_fetch_url(
+		string $provider,
+		string $url,
+		array $args = []
+	): string {
+		if ( 0 !== stripos( $provider, self::OEMBED_ENDPOINT ) ) {
+			return $provider;
+		}
+
+		if ( ! preg_match( self::OEMBED_FORMAT, $url, $matches ) ) {
+			return '';
+		}
+
+		$episode_slug_parts = explode( '-', $matches[2] );
+		$id                 = array_pop( $episode_slug_parts );
+
+		$provider = str_replace(
+			self::EPISODE_ID_PLACEHOLDER,
+			$id,
+			self::OEMBED_ENDPOINT
+		);
+
+		// Anchor.fm's oEmbed endpoint offers limited support for arguments.
+		if ( isset( $args['width'], $args['height'] ) ) {
+			$provider = add_query_arg(
+				[
+					'maxwidth'  => (int) $args['width'],
+					'maxheight' => (int) $args['height'],
+				],
+				$provider
+			);
+		}
+
+		return $provider;
+	}
+
+	/**
+	 * Render Anchor.fm iframe embed via a shortcode.
+	 *
+	 * @param array $attrs Shortcode attributes.
+	 * @return string
+	 */
+	public function do_shortcode( array $attrs ): string {
+		$attrs = shortcode_atts(
+			[
+				'src'    => null,
+				'url'    => null,
+				'width'  => '400px',
+				'height' => '102px',
+			],
+			$attrs,
+			self::SHORTCODE_TAG
+		);
+
+		// Fallback in case one passes `url` rather than `src`.
+		if ( empty( $attrs['src'] ) && ! empty( $attrs['url'] ) ) {
+			$attrs['src'] = $attrs['url'];
+		}
+
+		if ( empty( $attrs['src'] ) ) {
+			return '';
+		}
+
+		return sprintf(
+			'<iframe src="%1$s" width="%2$s" height="%3$s" frameborder="0" scrolling="no"></iframe>',
+			esc_url( $attrs['src'] ),
+			esc_attr( $attrs['width'] ),
+			esc_attr( $attrs['height'] )
+		);
+	}
+}
diff --git a/phpcs.xml b/phpcs.xml
index fc325fa8ef728942a9dbc570c845c1d0a968a881..ff47dc708d326f35790b403ba2915293ecd02306 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -26,7 +26,9 @@
 	<!-- 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="4.7"/>
-	<rule ref="WordPress" />
+	<rule ref="WordPress">
+		<exclude name="Generic.Arrays.DisallowShortArraySyntax" />
+	</rule>
 	<rule ref="WordPressVIPMinimum" />
 	<rule ref="WordPress-VIP-Go" />
 	<rule ref="WordPress.NamingConventions.PrefixAllGlobals">