diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 91de34a65356f0bcf316a310bca257028622a55d..bd50617eb8ebed69feee710784ca06b9870e3cf4 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -326,6 +326,14 @@ $routes->group(
                                 'filter' => 'permission:podcast_episodes-edit',
                             ]
                         );
+                        $routes->get(
+                            'embeddable-player',
+                            'Episode::embeddablePlayer/$1/$2',
+                            [
+                                'as' => 'embeddable-player-add',
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ]
+                        );
 
                         $routes->group('persons', function ($routes) {
                             $routes->get('/', 'EpisodePerson/$1/$2', [
@@ -565,9 +573,19 @@ $routes->group(config('App')->authGateway, function ($routes) {
 // Public routes
 $routes->group('@(:podcastName)', function ($routes) {
     $routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
-    $routes->get('(:slug)', 'Episode/$1/$2', [
-        'as' => 'episode',
-    ]);
+    $routes->group('(:slug)', function ($routes) {
+        $routes->get('/', 'Episode/$1/$2', [
+            'as' => 'episode',
+        ]);
+        $routes->group('embeddable-player', function ($routes) {
+            $routes->get('/', 'Episode::embeddablePlayer/$1/$2', [
+                'as' => 'embeddable-player',
+            ]);
+            $routes->get('(:slug)', 'Episode::embeddablePlayer/$1/$2/$3', [
+                'as' => 'embeddable-player-theme',
+            ]);
+        });
+    });
     $routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
     $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
 });
diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php
index 8ad5ce6f927be192fe74c486ae79fae286f1f455..92f9a8ea722934d7af9ca03648b93ec187a8bfcb 100644
--- a/app/Controllers/Admin/Episode.php
+++ b/app/Controllers/Admin/Episode.php
@@ -420,4 +420,21 @@ class Episode extends BaseController
             $this->episode->id,
         ]);
     }
+
+    public function embeddablePlayer()
+    {
+        helper(['form']);
+
+        $data = [
+            'podcast' => $this->podcast,
+            'episode' => $this->episode,
+            'themes' => EpisodeModel::$themes,
+        ];
+
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+            1 => $this->episode->title,
+        ]);
+        return view('admin/episode/embeddable_player', $data);
+    }
 }
diff --git a/app/Controllers/Admin/PodcastPlatform.php b/app/Controllers/Admin/PodcastPlatform.php
index 9229cbd053e8c87db4394cfaf31f0e61cc2c04a1..6f5fd99fd3821980383cd5ac670daaab2969a1f9 100644
--- a/app/Controllers/Admin/PodcastPlatform.php
+++ b/app/Controllers/Admin/PodcastPlatform.php
@@ -81,6 +81,12 @@ class PodcastPlatform extends BaseController
                     )
                         ? $podcastPlatform['visible'] == 'yes'
                         : false,
+                    'is_on_embeddable_player' => array_key_exists(
+                        'on_embeddable_player',
+                        $podcastPlatform
+                    )
+                        ? $podcastPlatform['on_embeddable_player'] == 'yes'
+                        : false,
                 ]);
             }
         }
diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php
index ccaa92f688a21cf6d451b652eb83e4ebee05756d..c687cb65c1dd908a0e68eb32de8fe7b03a60af90 100644
--- a/app/Controllers/Analytics.php
+++ b/app/Controllers/Analytics.php
@@ -49,8 +49,16 @@ class Analytics extends Controller
     public function hit($base64EpisodeData, ...$filename)
     {
         helper('media', 'analytics');
-
-        $serviceName = isset($_GET['_from']) ? $_GET['_from'] : '';
+        $session = \Config\Services::session();
+        $session->start();
+        $serviceName = '';
+        if (isset($_GET['_from'])) {
+            $serviceName = $_GET['_from'];
+        } elseif (!empty($session->get('embeddable_player_domain'))) {
+            $serviceName = $session->get('embeddable_player_domain');
+        } elseif ($session->get('referer') !== '- Direct -') {
+            $serviceName = parse_url($session->get('referer'), PHP_URL_HOST);
+        }
 
         $episodeData = unpack(
             'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 3df89dda870acb533787256340b2b827cf255778..477a5cad979cdf9ea35f05dc5ddf7e2eba722210 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -36,8 +36,9 @@ class Episode extends BaseController
         ) {
             throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
         }
-
-        return $this->$method();
+        unset($params[1]);
+        unset($params[0]);
+        return $this->$method(...$params);
     }
 
     public function index()
@@ -54,48 +55,12 @@ class Episode extends BaseController
                 $this->podcast->type
             );
 
+            helper(['persons']);
             $persons = [];
-            foreach ($this->episode->episode_persons as $episodePerson) {
-                if (array_key_exists($episodePerson->person->id, $persons)) {
-                    $persons[$episodePerson->person->id]['roles'] .=
-                        empty($episodePerson->person_group) ||
-                        empty($episodePerson->person_role)
-                            ? ''
-                            : (empty(
-                                    $persons[$episodePerson->person->id][
-                                        'roles'
-                                    ]
-                                )
-                                    ? ''
-                                    : ', ') .
-                                lang(
-                                    'PersonsTaxonomy.persons.' .
-                                        $episodePerson->person_group .
-                                        '.roles.' .
-                                        $episodePerson->person_role .
-                                        '.label'
-                                );
-                } else {
-                    $persons[$episodePerson->person->id] = [
-                        'full_name' => $episodePerson->person->full_name,
-                        'information_url' =>
-                            $episodePerson->person->information_url,
-                        'thumbnail_url' =>
-                            $episodePerson->person->image->thumbnail_url,
-                        'roles' =>
-                            empty($episodePerson->person_group) ||
-                            empty($episodePerson->person_role)
-                                ? ''
-                                : lang(
-                                    'PersonsTaxonomy.persons.' .
-                                        $episodePerson->person_group .
-                                        '.roles.' .
-                                        $episodePerson->person_role .
-                                        '.label'
-                                ),
-                    ];
-                }
-            }
+            construct_episode_person_array(
+                $this->episode->episode_persons,
+                $persons
+            );
 
             $data = [
                 'previousEpisode' => $previousNextEpisodes['previous'],
@@ -120,4 +85,58 @@ class Episode extends BaseController
 
         return $cachedView;
     }
+
+    public function embeddablePlayer($theme = 'light-transparent')
+    {
+        self::triggerWebpageHit($this->episode->podcast_id);
+
+        $session = \Config\Services::session();
+        $session->start();
+        if (isset($_SERVER['HTTP_REFERER'])) {
+            $session->set(
+                'embeddable_player_domain',
+                parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
+            );
+        }
+
+        $locale = service('request')->getLocale();
+
+        $cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
+
+        if (!($cachedView = cache($cacheName))) {
+            $episodeModel = new EpisodeModel();
+            $theme = EpisodeModel::$themes[$theme];
+            helper(['persons']);
+            $persons = [];
+            construct_episode_person_array(
+                $this->episode->episode_persons,
+                $persons
+            );
+            constructs_podcast_person_array(
+                $this->podcast->podcast_persons,
+                $persons
+            );
+
+            $data = [
+                'podcast' => $this->podcast,
+                'episode' => $this->episode,
+                'persons' => $persons,
+                'theme' => $theme,
+            ];
+
+            $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+                $this->podcast->id
+            );
+
+            // The page cache is set to a decade so it is deleted manually upon podcast update
+            return view('embeddable_player', $data, [
+                'cache' => $secondsToNextUnpublishedEpisode
+                    ? $secondsToNextUnpublishedEpisode
+                    : DECADE,
+                'cache_name' => $cacheName,
+            ]);
+        }
+
+        return $cachedView;
+    }
 }
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 9c6fc6756e19feca366c76f0233790a877ed03e3..d7a80a6ee3730d2079b3e2c7d1b79e2719b75010 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -109,48 +109,12 @@ class Podcast extends BaseController
                 ]);
             }
 
+            helper(['persons']);
             $persons = [];
-            foreach ($this->podcast->podcast_persons as $podcastPerson) {
-                if (array_key_exists($podcastPerson->person->id, $persons)) {
-                    $persons[$podcastPerson->person->id]['roles'] .=
-                        empty($podcastPerson->person_group) ||
-                        empty($podcastPerson->person_role)
-                            ? ''
-                            : (empty(
-                                    $persons[$podcastPerson->person->id][
-                                        'roles'
-                                    ]
-                                )
-                                    ? ''
-                                    : ', ') .
-                                lang(
-                                    'PersonsTaxonomy.persons.' .
-                                        $podcastPerson->person_group .
-                                        '.roles.' .
-                                        $podcastPerson->person_role .
-                                        '.label'
-                                );
-                } else {
-                    $persons[$podcastPerson->person->id] = [
-                        'full_name' => $podcastPerson->person->full_name,
-                        'information_url' =>
-                            $podcastPerson->person->information_url,
-                        'thumbnail_url' =>
-                            $podcastPerson->person->image->thumbnail_url,
-                        'roles' =>
-                            empty($podcastPerson->person_group) ||
-                            empty($podcastPerson->person_role)
-                                ? ''
-                                : lang(
-                                    'PersonsTaxonomy.persons.' .
-                                        $podcastPerson->person_group .
-                                        '.roles.' .
-                                        $podcastPerson->person_role .
-                                        '.label'
-                                ),
-                    ];
-                }
-            }
+            constructs_podcast_person_array(
+                $this->podcast->podcast_persons,
+                $persons
+            );
 
             $data = [
                 'podcast' => $this->podcast,
diff --git a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
index 045add8548aeb5844618560c801d5289f76a0b71..4b2a9f9efd48e1808fd3a3df097527b957222576 100644
--- a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
+++ b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
@@ -40,6 +40,11 @@ class AddPodcastsPlatforms extends Migration
                 'constraint' => 1,
                 'default' => 0,
             ],
+            'is_on_embeddable_player' => [
+                'type' => 'TINYINT',
+                'constraint' => 1,
+                'default' => 0,
+            ],
         ]);
 
         $this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 964d51b2c005bb26303b87bc8f3b1fee72d5b6a6..99bc4804e790f44b3d6ed1d732676ec8455fd067 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -94,6 +94,13 @@ class Episode extends Entity
      */
     protected $description;
 
+    /**
+     * The embeddable player URL
+     *
+     * @var string
+     */
+    protected $embeddable_player;
+
     /**
      * @var string
      */
@@ -421,6 +428,24 @@ class Episode extends Entity
         );
     }
 
+    public function getEmbeddablePlayer($theme = null)
+    {
+        return base_url(
+            $theme
+                ? route_to(
+                    'embeddable-player-theme',
+                    $this->getPodcast()->name,
+                    $this->attributes['slug'],
+                    $theme
+                )
+                : route_to(
+                    'embeddable-player',
+                    $this->getPodcast()->name,
+                    $this->attributes['slug']
+                )
+        );
+    }
+
     public function setGuid(string $guid)
     {
         return $this->attributes['guid'] = empty($guid)
diff --git a/app/Entities/Platform.php b/app/Entities/Platform.php
index 904603266341c52302f2032498528d1e8a0a4e0c..a1080da46f287f411ee4b3cfa3b377d732559d6f 100644
--- a/app/Entities/Platform.php
+++ b/app/Entities/Platform.php
@@ -21,5 +21,6 @@ class Platform extends Entity
         'link_url' => '?string',
         'link_content' => '?string',
         'is_visible' => '?boolean',
+        'is_on_embeddable_player' => '?boolean',
     ];
 }
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index f35cd75954a0f626e8a37e7ab52f1fbd1b60e994..aa4a4afe71a10dc6f92686931403ea2c12870b10 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -324,6 +324,24 @@ class Podcast extends Entity
         return $this->podcastingPlatforms;
     }
 
+    /**
+     * Returns true if the podcast has podcasting platform links
+     */
+    public function getHasPodcastingPlatforms()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcast must be created before getting podcasting platform.'
+            );
+        }
+        foreach ($this->getPodcastingPlatforms() as $podcastingPlatform) {
+            if ($podcastingPlatform->is_on_embeddable_player) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Returns the podcast's social platform links
      *
@@ -347,6 +365,24 @@ class Podcast extends Entity
         return $this->socialPlatforms;
     }
 
+    /**
+     * Returns true if the podcast has social platform links
+     */
+    public function getHasSocialPlatforms()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcast must be created before getting social platform.'
+            );
+        }
+        foreach ($this->getSocialPlatforms() as $socialPlatform) {
+            if ($socialPlatform->is_on_embeddable_player) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Returns the podcast's funding platform links
      *
@@ -370,6 +406,24 @@ class Podcast extends Entity
         return $this->fundingPlatforms;
     }
 
+    /**
+     * Returns true if the podcast has social platform links
+     */
+    public function getHasFundingPlatforms()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcast must be created before getting Funding platform.'
+            );
+        }
+        foreach ($this->getFundingPlatforms() as $fundingPlatform) {
+            if ($fundingPlatform->is_on_embeddable_player) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public function getOtherCategories()
     {
         if (empty($this->id)) {
diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php
index 7439c1e4f0ec84a705fa729fce379375188b9080..3360de83359b92aa2685142c03d269b5d0e762ae 100644
--- a/app/Helpers/analytics_helper.php
+++ b/app/Helpers/analytics_helper.php
@@ -111,9 +111,6 @@ function set_user_session_player()
     $session->start();
 
     if (!$session->has('player')) {
-        $session = \Config\Services::session();
-        $session->start();
-
         $playerFound = null;
         $userAgent = $_SERVER['HTTP_USER_AGENT'];
 
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index 3d69bab2ccda738debdab168801cc3ea015190fa..78b1516e4218ee01b3e12cd5b2a1e21822187624 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -384,29 +384,12 @@ if (!function_exists('location_link')) {
         $locationOsmid,
         $class = ''
     ) {
-        $link = null;
+        $link = '';
+
         if (!empty($locationName)) {
-            $uri = '';
-            if (!empty($locationOsmid)) {
-                $uri =
-                    'https://www.openstreetmap.org/' .
-                    ['N' => 'node', 'W' => 'way', 'R' => 'relation'][
-                        substr($locationOsmid, 0, 1)
-                    ] .
-                    '/' .
-                    substr($locationOsmid, 1);
-            } elseif (!empty($locationGeo)) {
-                $uri =
-                    'https://www.openstreetmap.org/#map=17/' .
-                    str_replace(',', '/', substr($locationGeo, 4));
-            } else {
-                $uri =
-                    'https://www.openstreetmap.org/search?query=' .
-                    urlencode($locationName);
-            }
             $link = button(
                 $locationName,
-                $uri,
+                location_url($locationName, $locationGeo, $locationOsmid),
                 [
                     'variant' => 'default',
                     'size' => 'small',
@@ -421,6 +404,7 @@ if (!function_exists('location_link')) {
                 ]
             );
         }
+
         return $link;
     }
 }
diff --git a/app/Helpers/persons_helper.php b/app/Helpers/persons_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..5fe79d763b5e39880a2353433ca719281a2f162a
--- /dev/null
+++ b/app/Helpers/persons_helper.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+/**
+ * Fetches persons from an episode
+ *
+ * @param array  $podcast_persons
+ * @param array    &$persons
+ */
+function constructs_podcast_person_array($podcast_persons, &$persons)
+{
+    foreach ($podcast_persons as $podcastPerson) {
+        if (array_key_exists($podcastPerson->person->id, $persons)) {
+            $persons[$podcastPerson->person->id]['roles'] .=
+                empty($podcastPerson->person_group) ||
+                empty($podcastPerson->person_role)
+                    ? ''
+                    : (empty($persons[$podcastPerson->person->id]['roles'])
+                            ? ''
+                            : ', ') .
+                        lang(
+                            'PersonsTaxonomy.persons.' .
+                                $podcastPerson->person_group .
+                                '.roles.' .
+                                $podcastPerson->person_role .
+                                '.label'
+                        );
+        } else {
+            $persons[$podcastPerson->person->id] = [
+                'full_name' => $podcastPerson->person->full_name,
+                'information_url' => $podcastPerson->person->information_url,
+                'thumbnail_url' => $podcastPerson->person->image->thumbnail_url,
+                'roles' =>
+                    empty($podcastPerson->person_group) ||
+                    empty($podcastPerson->person_role)
+                        ? ''
+                        : lang(
+                            'PersonsTaxonomy.persons.' .
+                                $podcastPerson->person_group .
+                                '.roles.' .
+                                $podcastPerson->person_role .
+                                '.label'
+                        ),
+            ];
+        }
+    }
+}
+
+/**
+ * Fetches persons from an episode
+ *
+ * @param array  $episode_persons
+ * @param array  &$persons
+ */
+function construct_episode_person_array($episode_persons, &$persons)
+{
+    foreach ($episode_persons as $episodePerson) {
+        if (array_key_exists($episodePerson->person->id, $persons)) {
+            $persons[$episodePerson->person->id]['roles'] .=
+                empty($episodePerson->person_group) ||
+                empty($episodePerson->person_role)
+                    ? ''
+                    : (empty($persons[$episodePerson->person->id]['roles'])
+                            ? ''
+                            : ', ') .
+                        lang(
+                            'PersonsTaxonomy.persons.' .
+                                $episodePerson->person_group .
+                                '.roles.' .
+                                $episodePerson->person_role .
+                                '.label'
+                        );
+        } else {
+            $persons[$episodePerson->person->id] = [
+                'full_name' => $episodePerson->person->full_name,
+                'information_url' => $episodePerson->person->information_url,
+                'thumbnail_url' => $episodePerson->person->image->thumbnail_url,
+                'roles' =>
+                    empty($episodePerson->person_group) ||
+                    empty($episodePerson->person_role)
+                        ? ''
+                        : lang(
+                            'PersonsTaxonomy.persons.' .
+                                $episodePerson->person_group .
+                                '.roles.' .
+                                $episodePerson->person_role .
+                                '.label'
+                        ),
+            ];
+        }
+    }
+}
diff --git a/app/Helpers/url_helper.php b/app/Helpers/url_helper.php
index ec462ca8e46be49f522ca3b6ed0cc99cd5c61f70..6fa434aefd44fb22fb6b544e245b0635416d0acd 100644
--- a/app/Helpers/url_helper.php
+++ b/app/Helpers/url_helper.php
@@ -38,3 +38,39 @@ if (!function_exists('current_season_url')) {
         return current_url() . $season_query_string;
     }
 }
+
+if (!function_exists('location_url')) {
+    /**
+     * Returns URL to display from location info
+     *
+     * @param string $locationName
+     * @param string $locationGeo
+     * @param string $locationOsmid
+     *
+     * @return string
+     */
+    function location_url($locationName, $locationGeo, $locationOsmid)
+    {
+        $uri = '';
+
+        if (!empty($locationOsmid)) {
+            $uri =
+                'https://www.openstreetmap.org/' .
+                ['N' => 'node', 'W' => 'way', 'R' => 'relation'][
+                    substr($locationOsmid, 0, 1)
+                ] .
+                '/' .
+                substr($locationOsmid, 1);
+        } elseif (!empty($locationGeo)) {
+            $uri =
+                'https://www.openstreetmap.org/#map=17/' .
+                str_replace(',', '/', substr($locationGeo, 4));
+        } elseif (!empty($locationName)) {
+            $uri =
+                'https://www.openstreetmap.org/search?query=' .
+                urlencode($locationName);
+        }
+
+        return $uri;
+    }
+}
diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php
index 03db0bce7f4e22e1c9257f41788613046373f423..ab1e511c9cbcf1c797cb2a7b42cca87840615a6e 100644
--- a/app/Language/en/Breadcrumb.php
+++ b/app/Language/en/Breadcrumb.php
@@ -32,4 +32,5 @@ return [
     'listening-time' => 'listening time',
     'time-periods' => 'time periods',
     'soundbites' => 'soundbites',
+    'embeddable-player' => 'embeddable player',
 ];
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index 00a487219f4e6e1e953b53e351a4cd7dc3d15525..18df1b40f447732545d440ffd5a6fa6058c5554b 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -109,4 +109,16 @@ return [
             'Click while playing to get current position, click again to get duration.',
         'submit_edit' => 'Save all soundbites',
     ],
+    'embeddable_player' => [
+        'add' => 'Add embeddable player',
+        'title' => 'Embeddable player',
+        'label' =>
+            'Pick a theme color, copy the embeddable player to clipboard, then paste it on your website.',
+        'clipboard_iframe' => 'Copy embeddable player to clipboard',
+        'clipboard_url' => 'Copy address to clipboard',
+        'dark' => 'Dark',
+        'dark-transparent' => 'Dark transparent',
+        'light' => 'Light',
+        'light-transparent' => 'Light transparent',
+    ],
 ];
diff --git a/app/Language/en/Platforms.php b/app/Language/en/Platforms.php
index da822c972d3abba33e536faaaab00f22ef322a25..c1890482cb63d3b39e30bc15fc1bad415864f04a 100644
--- a/app/Language/en/Platforms.php
+++ b/app/Language/en/Platforms.php
@@ -11,6 +11,7 @@ return [
     'home_url' => 'Go to {platformName} website',
     'submit_url' => 'Submit your podcast on {platformName}',
     'visible' => 'Display in podcast homepage?',
+    'on_embeddable_player' => 'Display on embeddable player?',
     'remove' => 'Remove {platformName}',
     'submit' => 'Save',
     'messages' => [
diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php
index 961d403c081fd1fbf64e571dbaca19a51c692132..179238e9f139ac144f90552bde3defc2e29d5d61 100644
--- a/app/Language/fr/Breadcrumb.php
+++ b/app/Language/fr/Breadcrumb.php
@@ -32,4 +32,5 @@ return [
     'listening-time' => 'drée d’écoute',
     'time-periods' => 'périodes',
     'soundbites' => 'extraits sonores',
+    'embeddable-player' => 'lecteur intégré',
 ];
diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php
index e691f9eed7401c2054296790734293b8072d98eb..78f94404c6bffa816e3aa1019085b444190a3bb6 100644
--- a/app/Language/fr/Episode.php
+++ b/app/Language/fr/Episode.php
@@ -111,4 +111,16 @@ return [
             'Cliquez pour récupérer la position actuelle, cliquez à nouveau pour récupérer la durée.',
         'submit_edit' => 'Enregistrer tous les extraits sonores',
     ],
+    'embeddable_player' => [
+        'add' => 'Ajouter un lecteur intégré',
+        'title' => 'Lecteur intégré',
+        'label' =>
+            'Sélectionnez une couleur de thème, copiez le code dans le presse-papier, puis collez-le sur votre site internet.',
+        'clipboard_iframe' => 'Copier le lecteur dans le presse papier',
+        'clipboard_url' => 'Copier l’adresse dans le presse papier',
+        'dark' => 'Sombre',
+        'dark-transparent' => 'Sombre transparent',
+        'light' => 'Clair',
+        'light-transparent' => 'Clair transparent',
+    ],
 ];
diff --git a/app/Language/fr/Platforms.php b/app/Language/fr/Platforms.php
index d7b66d639afa5f699e221ada146cda050f08f7ee..4a4fdc5d8814158d8d56e444812df871437f16c1 100644
--- a/app/Language/fr/Platforms.php
+++ b/app/Language/fr/Platforms.php
@@ -11,6 +11,7 @@ return [
     'home_url' => 'Aller au site {platformName}',
     'submit_url' => 'Soumettez votre podcast sur {platformName}',
     'visible' => 'Afficher sur la page d’accueil du podcast ?',
+    'on_embeddable_player' => 'Afficher sur le lecteur intégré ?',
     'remove' => 'Supprimer {platformName}',
     'submit' => 'Enregistrer',
     'messages' => [
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index eacbac9b3c2e91b4c4614fbde6126beda2d49961..3237daafa1b532b741690e45f5784c553ce2e375 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -69,6 +69,35 @@ class EpisodeModel extends Model
     protected $afterUpdate = ['writeEnclosureMetadata'];
     protected $beforeDelete = ['clearCache'];
 
+    public static $themes = [
+        'light-transparent' => [
+            'style' =>
+                'background-color: #fff; background-image: linear-gradient(45deg, #ccc 12.5%, transparent 12.5%, transparent 50%, #ccc 50%, #ccc 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;',
+            'background' => 'transparent',
+            'text' => '#000',
+            'inverted' => '#fff',
+        ],
+        'light' => [
+            'style' => 'background-color: #fff;',
+            'background' => '#fff',
+            'text' => '#000',
+            'inverted' => '#fff',
+        ],
+        'dark-transparent' => [
+            'style' =>
+                'background-color: #001f1a; background-image: linear-gradient(45deg, #888 12.5%, transparent 12.5%, transparent 50%, #888 50%, #888 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;',
+            'background' => 'transparent',
+            'text' => '#fff',
+            'inverted' => '#000',
+        ],
+        'dark' => [
+            'style' => 'background-color: #001f1a;',
+            'background' => '#001f1a',
+            'text' => '#fff',
+            'inverted' => '#000',
+        ],
+    ];
+
     public function getEpisodeBySlug($podcastId, $episodeSlug)
     {
         if (!($found = cache("podcast{$podcastId}_episode@{$episodeSlug}"))) {
@@ -411,6 +440,14 @@ class EpisodeModel extends Model
             }
         }
 
+        foreach (array_keys(self::$themes) as $themeKey) {
+            foreach ($supportedLocales as $locale) {
+                cache()->delete(
+                    "page_podcast{$episode->podcast_id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}"
+                );
+            }
+        }
+
         // delete query cache
         cache()->delete("podcast{$episode->podcast_id}_defaultQuery");
         cache()->delete("podcast{$episode->podcast_id}_years");
diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php
index 827c4de1fff0957b1254d7add4127d7357c2c5db..1307d1a42503488a6a07f31a4acb04e0391ce476 100644
--- a/app/Models/PlatformModel.php
+++ b/app/Models/PlatformModel.php
@@ -75,7 +75,7 @@ class PlatformModel extends Model
             !($found = cache("podcast{$podcastId}_platforms_{$platformType}"))
         ) {
             $found = $this->select(
-                'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible'
+                'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player'
             )
                 ->join(
                     'podcasts_platforms',
@@ -103,7 +103,7 @@ class PlatformModel extends Model
             ))
         ) {
             $found = $this->select(
-                'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible'
+                'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player'
             )
                 ->join(
                     'podcasts_platforms',
@@ -168,6 +168,8 @@ EOD;
 
     public function clearCache($podcastId)
     {
+        $podcast = (new PodcastModel())->getPodcastById($podcastId);
+
         foreach (['podcasting', 'social', 'funding'] as $platformType) {
             cache()->delete("podcast{$podcastId}_platforms_{$platformType}");
             cache()->delete(
@@ -195,5 +197,22 @@ EOD;
                 );
             }
         }
+
+        // clear cache for every localized podcast episode page
+        foreach ($podcast->episodes as $episode) {
+            foreach ($supportedLocales as $locale) {
+                cache()->delete(
+                    "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}"
+                );
+                foreach (
+                    array_keys(\App\Models\EpisodeModel::$themes)
+                    as $themeKey
+                ) {
+                    cache()->delete(
+                        "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}"
+                    );
+                }
+            }
+        }
     }
 }
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 4bfed81d2cf810f03a177f82ea69a0d885d3ac45..fc23d5dd02a1d312cde6feba11407ad627d18b6b 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -193,6 +193,14 @@ class PodcastModel extends Model
                 cache()->delete(
                     "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}"
                 );
+                foreach (
+                    array_keys(\App\Models\EpisodeModel::$themes)
+                    as $themeKey
+                ) {
+                    cache()->delete(
+                        "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}"
+                    );
+                }
             }
         }
         // clear cache for every credit page
diff --git a/app/Views/_assets/admin.ts b/app/Views/_assets/admin.ts
index 1c54fec275396cba346155ede9fe37c95988f5d4..8cb179e0016bd0b033e7a8f76cd693d8865c914a 100644
--- a/app/Views/_assets/admin.ts
+++ b/app/Views/_assets/admin.ts
@@ -1,4 +1,6 @@
 import ClientTimezone from "./modules/ClientTimezone";
+import Clipboard from "./modules/Clipboard";
+import ThemePicker from "./modules/ThemePicker";
 import DateTimePicker from "./modules/DateTimePicker";
 import Dropdown from "./modules/Dropdown";
 import MarkdownEditor from "./modules/MarkdownEditor";
@@ -19,3 +21,5 @@ ClientTimezone();
 DateTimePicker();
 Time();
 Soundbites();
+Clipboard();
+ThemePicker();
diff --git a/app/Views/_assets/icons/file-copy.svg b/app/Views/_assets/icons/file-copy.svg
new file mode 100644
index 0000000000000000000000000000000000000000..491df11d372ab1a6668a7193a78734abd4a96181
--- /dev/null
+++ b/app/Views/_assets/icons/file-copy.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1H7zM5.002 8L5 20h10V8H5.002zM9 6h8v10h2V4H9v2zm-2 5h6v2H7v-2zm0 4h6v2H7v-2z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/icons/movie.svg b/app/Views/_assets/icons/movie.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a3eaa1b73d707fc82251ced6f3ce081acff9c7cd
--- /dev/null
+++ b/app/Views/_assets/icons/movie.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM4 5v14h16V5H4zm6.622 3.415l4.879 3.252a.4.4 0 0 1 0 .666l-4.88 3.252a.4.4 0 0 1-.621-.332V8.747a.4.4 0 0 1 .622-.332z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/modules/Clipboard.ts b/app/Views/_assets/modules/Clipboard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b5b0a59acb02a5ee84ee0155316f0443e43cd62
--- /dev/null
+++ b/app/Views/_assets/modules/Clipboard.ts
@@ -0,0 +1,23 @@
+const Clipboard = (): void => {
+  const buttons: NodeListOf<
+    HTMLButtonElement
+  > | null = document.querySelectorAll("button[data-type='clipboard-copy']");
+
+  if (buttons) {
+    for (let i = 0; i < buttons.length; i++) {
+      const button: HTMLButtonElement = buttons[i];
+      const textArea: HTMLTextAreaElement | null = document.querySelector(
+        `textarea[id="${button.dataset.clipboardTarget}"]`
+      );
+      if (textArea) {
+        button.addEventListener("click", () => {
+          textArea.select();
+          textArea.setSelectionRange(0, textArea.value.length);
+          document.execCommand("copy");
+        });
+      }
+    }
+  }
+};
+
+export default Clipboard;
diff --git a/app/Views/_assets/modules/ThemePicker.ts b/app/Views/_assets/modules/ThemePicker.ts
new file mode 100644
index 0000000000000000000000000000000000000000..167f2600206df9cb590573de5af6578ff5b316ae
--- /dev/null
+++ b/app/Views/_assets/modules/ThemePicker.ts
@@ -0,0 +1,30 @@
+const ThemePicker = (): void => {
+  const buttons: NodeListOf<
+    HTMLButtonElement
+  > | null = document.querySelectorAll("button[data-type='theme-picker']");
+  const iframe: HTMLIFrameElement | null = document.querySelector(
+    `iframe[id="embeddable_player"]`
+  );
+  const iframeTextArea: HTMLTextAreaElement | null = document.querySelector(
+    `textarea[id="iframe"]`
+  );
+  const urlTextArea: HTMLTextAreaElement | null = document.querySelector(
+    `textarea[id="url"]`
+  );
+
+  if (buttons && iframe && iframeTextArea && urlTextArea) {
+    for (let i = 0; i < buttons.length; i++) {
+      const button: HTMLButtonElement = buttons[i];
+      const url: string | undefined = button.dataset.url;
+      if (url) {
+        button.addEventListener("click", () => {
+          iframeTextArea.value = `<iframe width="100%" height="280" frameborder="0" scrolling="no" style="width: 100%; height: 280px;  overflow: hidden;" src="${url}"></iframe>`;
+          urlTextArea.value = url;
+          iframe.src = url;
+        });
+      }
+    }
+  }
+};
+
+export default ThemePicker;
diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php
new file mode 100644
index 0000000000000000000000000000000000000000..2144580e1e003acf702f145de1cf68f8a5ae420a
--- /dev/null
+++ b/app/Views/admin/episode/embeddable_player.php
@@ -0,0 +1,66 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Episode.embeddable_player.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Episode.embeddable_player.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+    <?= form_label(lang('Episode.embeddable_player.label'), 'label') ?>
+
+    <div class="flex w-full mt-6 mb-6">
+        <?php foreach ($themes as $themeKey => $theme): ?>
+            <button style="<?= $theme[
+                'style'
+            ] ?>" class="w-12 h-12 mr-1 border-2 border-gray-400 rounded-lg hover:border-white" title="<?= lang("Episode.embeddable_player.{$themeKey}") ?>" data-type="theme-picker" data-url="<?= $episode->getEmbeddablePlayer(
+    $themeKey
+) ?>"></button>
+        <?php endforeach; ?>
+    </div>
+
+    <iframe name="embeddable_player" id="embeddable_player" width="100%" height="280" frameborder="0" scrolling="no" style="width: 100%; height: 280;  overflow: hidden;" src="<?= $episode->embeddable_player ?>"></iframe>
+
+    <div class="flex items-center w-full mt-8">
+        <?= form_textarea(
+            [
+                'id' => 'iframe',
+                'name' => 'iframe',
+                'class' => 'form-textarea w-full h-20 mr-2',
+            ],
+            "<iframe width=\"100%\" height=\"280\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: 280px; overflow: hidden;\" src=\"{$episode->embeddable_player}\"></iframe>"
+        ) ?>
+        <?= icon_button(
+            'file-copy',
+            lang('Episode.embeddable_player.clipboard_iframe'),
+            null,
+            ['variant' => 'default'],
+            [
+                'data-type' => 'clipboard-copy',
+                'data-clipboard-target' => 'iframe',
+            ]
+        ) ?>
+    </div>
+
+    <div class="flex items-center w-full mt-4">
+       <?= form_textarea(
+           [
+               'id' => 'url',
+               'name' => 'url',
+               'class' => 'form-textarea w-full h-10 mr-2',
+           ],
+           $episode->embeddable_player
+       ) ?>
+        <?= icon_button(
+            'file-copy',
+            lang('Episode.embeddable_player.clipboard_url'),
+            null,
+            ['variant' => 'default'],
+            ['data-type' => 'clipboard-copy', 'data-clipboard-target' => 'url']
+        ) ?>
+    </div>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php
index 6bed3c358aed1680d5bc611ea732bf848eb3e4b2..fc0f61e37348abbffe019f965a91a69c4651e304 100644
--- a/app/Views/admin/episode/list.php
+++ b/app/Views/admin/episode/list.php
@@ -61,6 +61,13 @@
                                         $podcast->id,
                                         $episode->id
                                     ) ?>"><?= lang('Episode.edit') ?></a>
+                                    <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                        'embeddable-player-add',
+                                        $podcast->id,
+                                        $episode->id
+                                    ) ?>"><?= lang(
+    'Episode.embeddable_player.add'
+) ?></a>
                                     <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
                                         'episode-person-manage',
                                         $podcast->id,
diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php
index 08956ec37c83e7c8be6fb287445d167fa5cdd629..bc2dfd4641148173ef3a8146884949e4969c759a 100644
--- a/app/Views/admin/episode/view.php
+++ b/app/Views/admin/episode/view.php
@@ -58,6 +58,12 @@
 </div>
 
     <div class="mb-12">
+    <?= button(
+        lang('Episode.embeddable_player.add'),
+        route_to('embeddable-player-add', $podcast->id, $episode->id),
+        ['variant' => 'info', 'iconLeft' => 'movie'],
+        ['class' => 'mb-4']
+    ) ?>
     <?= button(
         lang('Episode.soundbites_form.title'),
         route_to('soundbites-edit', $podcast->id, $episode->id),
diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php
index 7c6f17dba3a7bb0bc6c463b1c355d8f7a13afa62..71ec4389ba174c73e91791158a0b7944f5ec28d1 100644
--- a/app/Views/admin/podcast/latest_episodes.php
+++ b/app/Views/admin/podcast/latest_episodes.php
@@ -58,6 +58,13 @@
                                     $podcast->id,
                                     $episode->id
                                 ) ?>"><?= lang('Episode.edit') ?></a>
+                                <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                    'embeddable-player-add',
+                                    $podcast->id,
+                                    $episode->id
+                                ) ?>"><?= lang(
+    'Episode.embeddable_player.add'
+) ?></a>
                                 <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
                                     'episode-person-manage',
                                     $podcast->id,
diff --git a/app/Views/admin/podcast/platforms.php b/app/Views/admin/podcast/platforms.php
index 06e3df6fab7f01b51fa53f0d89233cc4418d11f6..88365ad8bd4203a3463da18271c37e626ba5be2c 100644
--- a/app/Views/admin/podcast/platforms.php
+++ b/app/Views/admin/podcast/platforms.php
@@ -115,6 +115,22 @@
                 $platform->slug . '_visible',
                 $platform->is_visible ? $platform->is_visible : false
             ),
+            'text-sm mb-1'
+        ) ?>
+        <?= form_switch(
+            lang('Platforms.on_embeddable_player'),
+            [
+                'id' => $platform->slug . '_on_embeddable_player',
+                'name' =>
+                    'platforms[' . $platform->slug . '][on_embeddable_player]',
+            ],
+            'yes',
+            old(
+                $platform->slug . '_on_embeddable_player',
+                $platform->is_on_embeddable_player
+                    ? $platform->is_on_embeddable_player
+                    : false
+            ),
             'text-sm'
         ) ?>
     </div>
diff --git a/app/Views/embeddable_player.php b/app/Views/embeddable_player.php
new file mode 100644
index 0000000000000000000000000000000000000000..a20367325969bed877b8961db872259b8231414a
--- /dev/null
+++ b/app/Views/embeddable_player.php
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html lang="<?= service('request')->getLocale() ?>">
+<head>
+    <meta charset="UTF-8" />
+    <title><?= $episode->title ?></title>
+    <meta name="description"
+    content="<?= htmlspecialchars($episode->description) ?>"/> 
+    <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
+    <link rel="stylesheet" href="/assets/index.css" />
+    <link rel="canonical" href="<?= $episode->link ?>" />
+</head>
+<body>
+    <div class="flex w-full p-1 md:p-2"style="background: <?= $theme[
+        'background'
+    ] ?>; color: <?= $theme['text'] ?>;">
+        <img src="<?= $episode->image
+            ->medium_url ?>" alt="<?= $episode->title ?>" class="w-32 h-32 md:w-64 md:h-64" />
+        <div class="flex-grow pl-4">
+            <div class="flex">
+                <a href="<?= route_to('podcast', $podcast->name) ?>"
+                style="color: <?= $theme['text'] ?>;"
+                class="flex flex-col text-base leading-tight opacity-50 md:text-lg hover:opacity-100" target="_blank">
+                <?= $podcast->title ?>
+                </a>
+                <address class="ml-2 text-xs opacity-50 md:text-sm">
+                <?= lang('Podcast.by', [
+                    'publisher' => $podcast->publisher,
+                ]) ?></address>
+            </div>
+
+            <div class="flex mt-1 space-x-2 md:space-x-4 md:mt-3 md:top-0 md:mr-4 md:right-0 md:absolute ">
+                <?php if ($podcast->has_social_platforms): ?> 
+                    <div  class="flex space-x-1">
+                        <?php foreach (
+                            $podcast->social_platforms
+                            as $socialPlatform
+                        ): ?>
+                            <?php if (
+                                $socialPlatform->is_on_embeddable_player
+                            ): ?>
+                                <?= anchor(
+                                    $socialPlatform->link_url,
+                                    platform_icon(
+                                        $socialPlatform->type,
+                                        $socialPlatform->slug,
+                                        'h-4 md:h-6'
+                                    ),
+                                    [
+                                        'target' => '_blank',
+                                        'rel' => 'noopener noreferrer',
+                                        'title' => $socialPlatform->label,
+                                        'class' =>
+                                            'opacity-50 hover:opacity-100',
+                                    ]
+                                ) ?>
+                            <?php endif; ?>
+                        <?php endforeach; ?>
+                    </div>
+                <?php endif; ?>
+                <?php if ($podcast->has_funding_platforms): ?>
+                    <div class="flex space-x-1">
+                        <?php foreach (
+                            $podcast->funding_platforms
+                            as $fundingPlatform
+                        ): ?>
+                            <?php if (
+                                $fundingPlatform->is_on_embeddable_player
+                            ): ?>
+                                <?= anchor(
+                                    $fundingPlatform->link_url,
+                                    platform_icon(
+                                        $fundingPlatform->type,
+                                        $fundingPlatform->slug,
+                                        'h-4 md:h-6'
+                                    ),
+                                    [
+                                        'target' => '_blank',
+                                        'rel' => 'noopener noreferrer',
+                                        'title' => $fundingPlatform->label,
+                                        'class' =>
+                                            'opacity-50 hover:opacity-100',
+                                    ]
+                                ) ?>
+                            <?php endif; ?>
+                        <?php endforeach; ?>
+                    </div>
+                <?php endif; ?>
+                <div class="flex space-x-1">
+                <?php foreach (
+                    $podcast->podcasting_platforms
+                    as $podcastingPlatform
+                ): ?>
+                    <?php if ($podcastingPlatform->is_on_embeddable_player): ?>
+                        <?= anchor(
+                            $podcastingPlatform->link_url,
+                            platform_icon(
+                                $podcastingPlatform->type,
+                                $podcastingPlatform->slug,
+                                'h-4 md:h-6'
+                            ),
+                            [
+                                'target' => '_blank',
+                                'rel' => 'noopener noreferrer',
+                                'title' => $podcastingPlatform->label,
+                                'class' => 'opacity-50 hover:opacity-100',
+                            ]
+                        ) ?>
+                    <?php endif; ?>
+                <?php endforeach; ?>
+                <?= anchor(
+                    route_to('podcast_feed', $podcast->name),
+                    icon('rss', 'mr-2') . lang('Podcast.feed'),
+                    [
+                        'target' => '_blank',
+                        'class' =>
+                            'text-white h-4 md:h-6 md:text-sm text-xs bg-gradient-to-r from-orange-400 to-red-500 hover:to-orange-500 hover:bg-orange-500 inline-flex items-center px-2 py-1 font-semibold rounded-md md:rounded-lg shadow-md hover:bg-orange-600',
+                    ]
+                ) ?>
+                </div>
+            </div>
+            <h1 class="mt-2 text-xl font-semibold opacity-100 md:text-3xl hover:opacity-75">
+                <a href="<?= $episode->link ?>"
+                style="color: <?= $theme['text'] ?>;"
+                target="_blank">
+                    <?= $episode->title ?>
+                </a>
+            </h1>
+            <div class="flex w-full">
+                <div
+                    style="color: <?= $theme['text'] ?>;"
+                    class="text-sm opacity-50 md:text-base">
+                    <?= episode_numbering(
+                        $episode->number,
+                        $episode->season_number
+                    ) ?>
+                    <div>
+                        <time
+                            pubdate
+                            datetime="<?= $episode->published_at->format(
+                                DateTime::ATOM
+                            ) ?>"
+                            title="<?= $episode->published_at ?>">
+                            <?= lang('Common.mediumDate', [
+                                $episode->published_at,
+                            ]) ?>
+                        </time>
+                        <span>•</span>
+                        <time datetime="PT<?= $episode->enclosure_duration ?>S">
+                            <?= format_duration($episode->enclosure_duration) ?>
+                        </time>
+                    </div>
+                </div>
+                <?php if ($episode->location_name): ?>
+                    <a href="<?= location_url(
+                        $episode->location_name,
+                        $episode->location_geo,
+                        $episode->location_osmid
+                    ) ?>"
+                style="color: <?= $theme['inverted'] ?>; background: <?= $theme[
+    'text'
+] ?>;" class="inline-flex items-center px-3 py-1 mt-1 ml-4 text-xs align-middle rounded-full shadow-xs outline-none opacity-50 md:mt-2 md:text-sm hover:opacity-75 focus:shadow-outline" target="_blank" rel="noreferrer noopener"><?= icon(
+    'map-pin'
+) ?>
+                        <?= $episode->location_name ?>
+                    </a>
+                <?php endif; ?>
+            </div>
+                    
+            <?php if (!empty($persons)): ?>
+                <div class="flex my-2 space-x-1 md:my-4 md:space-x-2">
+                    <?php foreach ($persons as $person): ?>
+                        <?php if (!empty($person['information_url'])): ?>
+                            <a href="<?= $person['information_url'] ?>"
+                            class="hover:opacity-50"
+                            target="_blank"
+                            rel="noreferrer noopener">
+                        <?php endif; ?>
+                                <img src="<?= $person['thumbnail_url'] ?>"
+                                alt="<?= $person['full_name'] ?>"
+                                title="[<?= $person[
+                                    'full_name'
+                                ] ?>] <?= $person['roles'] ?>"
+                                class="object-cover h-8 rounded-full md:h-12 md:w-12" />
+                        <?php if (!empty($person['information_url'])): ?>
+                            </a>
+                        <?php endif; ?>
+                    <?php endforeach; ?>
+                </div>
+            <?php endif; ?>
+            <audio controls preload="none" class="flex w-full mt-2 md:mt-4">
+                <source
+                src="<?= $episode->enclosure_url .
+                    (isset($_SERVER['HTTP_REFERER'])
+                        ? '?_from=' .
+                            parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
+                        : '') ?>"
+                type="<?= $episode->enclosure_type ?>" />
+                    Your browser does not support the audio tag.
+            </audio>
+        </div>
+
+
+        <a href="https://castopod.org/"
+        class="absolute bottom-0 right-0 mb-4 mr-4 hover:opacity-75"
+        title="<?= lang('Common.powered_by', [
+            'castopod' => 'Castopod',
+        ]) ?>"
+        target="_blank"
+        rel="noopener noreferrer">
+            <?= platform_icon('podcasting', 'castopod', 'h-6') ?>
+        </a>
+    </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/app/Views/episode.php b/app/Views/episode.php
index 0b116d7bd4bf97c8bd4dbd89dd8e96af283668da..5570d4e7fa93e791eea55056a2c7cbeb720353df 100644
--- a/app/Views/episode.php
+++ b/app/Views/episode.php
@@ -5,15 +5,16 @@
 <head>
     <meta charset="UTF-8"/>
     <title><?= $episode->title ?></title>
-    <meta name="description" content="<?= $episode->description ?>"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <meta name="description"
+    content="<?= htmlspecialchars($episode->description) ?>" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <?php if (
     !empty($podcast->payment_pointer)
-): ?>    <meta name="monetization" content="<?= $podcast->payment_pointer ?>">                    
+): ?>    <meta name="monetization" content="<?= $podcast->payment_pointer ?>" />                    
 <?php endif; ?>
     <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
     <link rel="stylesheet" href="/assets/index.css"/>
-    <link rel="canonical" href="<?= current_url() ?>" />
+    <link rel="canonical" href="<?= $episode->link ?>" />
     <script src="/assets/podcast.js" type="module" defer></script>
     <meta property="og:title" content="<?= $episode->title ?>" />
     <meta property="og:locale" content="<?= $podcast->language_code ?>" />
diff --git a/app/Views/podcast.php b/app/Views/podcast.php
index 29fed4b48ed4d97e1dd8547bab98ebd8c313c83c..2b42958621db83b0a74be42ae8fa9d2531c9ec85 100644
--- a/app/Views/podcast.php
+++ b/app/Views/podcast.php
@@ -6,11 +6,12 @@
 <head>
     <meta charset="UTF-8"/>
     <title><?= $podcast->title ?></title>
-    <meta name="description" content="<?= $podcast->description ?>"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <meta name="description"
+    content="<?= htmlspecialchars($podcast->description) ?>" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <?php if (
     !empty($podcast->payment_pointer)
-): ?>    <meta name="monetization" content="<?= $podcast->payment_pointer ?>">                    
+): ?>    <meta name="monetization" content="<?= $podcast->payment_pointer ?>" />                    
 <?php endif; ?>
     <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
     <link rel="stylesheet" href="/assets/index.css"/>