diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index d46a8855682d95c729a092ae238f396c239ddaa1..ae3c9365b1103e1f9dc91fddc87232be22fa2186 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -210,6 +210,15 @@ $routes->get('audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioControl
     'as' => 'episode-audio',
 ], );
 
+// episode preview link
+$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
+    'as' => 'episode-preview',
+]);
+
+$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
+    'as' => 'episode-preview-activity',
+]);
+
 // Other pages
 $routes->get('/credits', 'CreditsController', [
     'as' => 'credits',
diff --git a/app/Controllers/EpisodePreviewController.php b/app/Controllers/EpisodePreviewController.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c03eef23a0cdb4f48e5a30756dea54e084663cb
--- /dev/null
+++ b/app/Controllers/EpisodePreviewController.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright  2023 Ad Aures
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers;
+
+use App\Entities\Episode;
+use App\Models\EpisodeModel;
+use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\HTTP\RedirectResponse;
+
+class EpisodePreviewController extends BaseController
+{
+    protected Episode $episode;
+
+    public function _remap(string $method, string ...$params): mixed
+    {
+        if (count($params) < 1) {
+            throw PageNotFoundException::forPageNotFound();
+        }
+
+        // find episode by previewUUID
+        $episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
+
+        if (! $episode instanceof Episode) {
+            throw PageNotFoundException::forPageNotFound();
+        }
+
+        $this->episode = $episode;
+
+        if ($episode->publication_status === 'published') {
+            // redirect to episode page
+            return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
+        }
+
+        unset($params[0]);
+
+        return $this->{$method}(...$params);
+    }
+
+    public function index(): RedirectResponse | string
+    {
+        helper('form');
+
+        return view('episode/preview-comments', [
+            'podcast' => $this->episode->podcast,
+            'episode' => $this->episode,
+        ]);
+    }
+
+    public function activity(): RedirectResponse | string
+    {
+        helper('form');
+
+        return view('episode/preview-activity', [
+            'podcast' => $this->episode->podcast,
+            'episode' => $this->episode,
+        ]);
+    }
+}
diff --git a/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php
new file mode 100644
index 0000000000000000000000000000000000000000..2bd026e2751963cd501c933b2df13a1fa9d4eedb
--- /dev/null
+++ b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Database\Migrations;
+
+class AddEpisodePreviewId extends BaseMigration
+{
+    public function up(): void
+    {
+        $fields = [
+            'preview_id' => [
+                'type'       => 'BINARY',
+                'constraint' => 16,
+                'after'      => 'podcast_id',
+            ],
+        ];
+
+        $this->forge->addColumn('episodes', $fields);
+
+        // set preview_id as unique key
+        $prefix = $this->db->getPrefix();
+        $uniquePreviewId = <<<CODE_SAMPLE
+            ALTER TABLE `{$prefix}episodes`
+            ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`);
+        CODE_SAMPLE;
+
+        $this->db->query($uniquePreviewId);
+    }
+
+    public function down(): void
+    {
+        $fields = ['preview_id'];
+        $this->forge->dropColumn('episodes', $fields);
+    }
+}
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 41c54a5bc72775ef405bf51f1657783a56666873..c7af02d3f007ce66b9d9f25074c056902ac6805f 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -14,6 +14,7 @@ use App\Entities\Clip\Soundbite;
 use App\Libraries\SimpleRSSElement;
 use App\Models\ClipModel;
 use App\Models\EpisodeCommentModel;
+use App\Models\EpisodeModel;
 use App\Models\PersonModel;
 use App\Models\PodcastModel;
 use App\Models\PostModel;
@@ -21,6 +22,7 @@ use CodeIgniter\Entity\Entity;
 use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use CodeIgniter\I18n\Time;
+use Exception;
 use League\CommonMark\Environment\Environment;
 use League\CommonMark\Extension\Autolink\AutolinkExtension;
 use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
@@ -39,6 +41,8 @@ use SimpleXMLElement;
  * @property int $id
  * @property int $podcast_id
  * @property Podcast $podcast
+ * @property ?string $preview_id
+ * @property string $preview_link
  * @property string $link
  * @property string $guid
  * @property string $slug
@@ -150,6 +154,7 @@ class Episode extends Entity
     protected $casts = [
         'id'                    => 'integer',
         'podcast_id'            => 'integer',
+        'preview_id'            => '?string',
         'guid'                  => 'string',
         'slug'                  => 'string',
         'title'                 => 'string',
@@ -509,7 +514,7 @@ class Episode extends Entity
 
         if ($this->getPodcast()->episode_description_footer_html) {
             $descriptionHtml .= "<footer>{$this->getPodcast()
-                ->episode_description_footer_html}</footer>";
+->episode_description_footer_html}</footer>";
         }
 
         return $descriptionHtml;
@@ -667,4 +672,18 @@ class Episode extends Entity
             urlencode((string) $this->attributes['guid']) .
             ($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
     }
+
+    public function getPreviewLink(): string
+    {
+        if ($this->preview_id === null) {
+            // generate preview id
+            if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) {
+                throw new Exception('Could not set episode preview id');
+            }
+
+            $this->preview_id = $previewUUID;
+        }
+
+        return url_to('episode-preview', (string) $this->preview_id);
+    }
 }
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index 351d2ec04e8652df6098b49e06060b481055ed2a..176e75c7ae3d4497e2c7859afbf791ff0c8aab77 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
  */
 
 use App\Entities\Category;
+use App\Entities\Episode;
 use App\Entities\Location;
 use CodeIgniter\I18n\Time;
 use CodeIgniter\View\Table;
@@ -218,8 +219,8 @@ if (! function_exists('publication_status_banner')) {
         }
 
         return <<<HTML
-        <div class="flex items-center px-12 py-2 border-b bg-stripes-gray border-subtle" role="alert">
-            <p class="flex items-center text-gray-900">
+        <div class="flex flex-wrap items-baseline px-4 py-2 border-b md:px-12 bg-stripes-default border-subtle" role="alert">
+            <p class="flex items-baseline text-gray-900">
                 <span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
                 <span class="ml-3 text-sm">{$bannerText}</span>
             </p>
@@ -231,6 +232,58 @@ if (! function_exists('publication_status_banner')) {
 
 // ------------------------------------------------------------------------
 
+if (! function_exists('episode_publication_status_banner')) {
+    /**
+     * Publication status banner component for podcasts
+     *
+     * Displays the appropriate banner depending on the podcast's publication status.
+     */
+    function episode_publication_status_banner(Episode $episode, string $class = ''): string
+    {
+        switch ($episode->publication_status) {
+            case 'not_published':
+                $linkRoute = route_to('episode-publish', $episode->podcast_id, $episode->id);
+                $publishLinkLabel = lang('Episode.publish');
+                break;
+            case 'scheduled':
+            case 'with_podcast':
+                $linkRoute = route_to('episode-publish_edit', $episode->podcast_id, $episode->id);
+                $publishLinkLabel = lang('Episode.publish_edit');
+                break;
+            default:
+                $bannerDisclaimer = '';
+                $linkRoute = '';
+                $publishLinkLabel = '';
+                break;
+        }
+
+        $bannerDisclaimer = lang('Episode.publication_status_banner.draft_mode');
+        $bannerText = lang('Episode.publication_status_banner.text', [
+            'publication_status' => $episode->publication_status,
+            'publication_date'   => $episode->published_at instanceof Time ? local_datetime(
+                $episode->published_at
+            ) : null,
+        ], null, false);
+        $previewLinkLabel = lang('Episode.publication_status_banner.preview');
+
+        return <<<HTML
+        <div class="flex flex-wrap gap-4 items-baseline px-4 md:px-12 py-2 bg-stripes-default border-subtle {$class}" role="alert">
+            <p class="flex items-baseline text-gray-900">
+                <span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
+                <span class="ml-3 text-sm">{$bannerText}</span>
+            </p>
+            <div class="flex items-baseline">    
+                <a href="{$episode->preview_link}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$previewLinkLabel}</a>
+                <span class="mx-1">•</span>
+                <a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$publishLinkLabel}</a>
+            </div>
+        </div>
+        HTML;
+    }
+}
+
+// ------------------------------------------------------------------------
+
 if (! function_exists('episode_numbering')) {
     /**
      * Returns relevant translated episode numbering.
@@ -360,7 +413,7 @@ if (! function_exists('relative_time')) {
         $datetime = $time->format(DateTime::ATOM);
 
         return <<<HTML
-            <relative-time tense="past" class="{$class}" datetime="{$datetime}">
+            <relative-time tense="auto" class="{$class}" datetime="{$datetime}">
                 <time
                     datetime="{$datetime}"
                     title="{$time}">{$translatedDate}</time>
@@ -378,10 +431,10 @@ if (! function_exists('local_datetime')) {
             'request'
         )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
         $translatedDate = $time->toLocalizedString($formatter->getPattern());
-        $datetime = $time->format(DateTime::ISO8601);
+        $datetime = $time->format(DateTime::ATOM);
 
         return <<<HTML
-            <relative-time datetime="{$datetime}" 
+            <relative-time datetime="{$datetime}"
                 prefix=""
                 threshold="PT0S"
                 weekday="long"
diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php
index bf91158b9d73410e86775dde2f93081968ca28b5..08d8f14ac07cf145bbf0c3b10754bd194678cf4d 100644
--- a/app/Helpers/id3_helper.php
+++ b/app/Helpers/id3_helper.php
@@ -82,14 +82,12 @@ if (! function_exists('write_audio_file_tags')) {
 
         // write tags
         if ($tagwriter->WriteTags()) {
-            echo 'Successfully wrote tags<br>';
+            // Successfully wrote tags
             if ($tagwriter->warnings !== []) {
-                echo 'There were some warnings:<br>' .
-                    implode('<br><br>', $tagwriter->warnings);
+                log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings));
             }
         } else {
-            echo 'Failed to write tags!<br>' .
-                implode('<br><br>', $tagwriter->errors);
+            log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors));
         }
     }
 }
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index ebe39336fc45d6f567b74c6a03c131b981afdc36..44be8e384d5d5868f1e92246b35554fe5344b7c2 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -30,4 +30,16 @@ return [
     }',
     'all_podcast_episodes' => 'All podcast episodes',
     'back_to_podcast' => 'Go back to podcast',
+    'preview' => [
+        'title' => 'Preview',
+        'not_published' => 'Not published',
+        'text' => '{publication_status, select,
+            published {This episode is not yet published.}
+            scheduled {This episode is scheduled for publication on {publication_date}.}
+            with_podcast {This episode will be published at the same time as the podcast.}
+            other {This episode is not yet published.}
+        }',
+        'publish' => 'Publish',
+        'publish_edit' => 'Edit publication',
+    ],
 ];
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index bedb194998f355037a5781696fc3b2bb44badc7a..bde68905fedf3474c36a8a5dc01f42c0a1d42384 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -14,9 +14,10 @@ use App\Entities\Episode;
 use CodeIgniter\Database\BaseBuilder;
 use CodeIgniter\Database\BaseResult;
 use CodeIgniter\I18n\Time;
-use CodeIgniter\Model;
+use Michalsn\Uuid\UuidModel;
+use Ramsey\Uuid\Lazy\LazyUuidFromString;
 
-class EpisodeModel extends Model
+class EpisodeModel extends UuidModel
 {
     /**
      * TODO: remove, shouldn't be here
@@ -50,6 +51,11 @@ class EpisodeModel extends Model
         ],
     ];
 
+    /**
+     * @var string[]
+     */
+    protected $uuidFields = ['preview_id'];
+
     /**
      * @var string
      */
@@ -61,6 +67,7 @@ class EpisodeModel extends Model
     protected $allowedFields = [
         'id',
         'podcast_id',
+        'preview_id',
         'guid',
         'title',
         'slug',
@@ -188,6 +195,38 @@ class EpisodeModel extends Model
         return $found;
     }
 
+    public function getEpisodeByPreviewId(string $previewId): ?Episode
+    {
+        $cacheName = "podcast_episode#preview-{$previewId}";
+        if (! ($found = cache($cacheName))) {
+            $builder = $this->where([
+                'preview_id' => $this->uuid->fromString($previewId)
+                    ->getBytes(),
+            ]);
+
+            $found = $builder->first();
+
+            cache()
+                ->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    public function setEpisodePreviewId(int $episodeId): string|false
+    {
+        /** @var LazyUuidFromString $uuid */
+        $uuid = $this->uuid->{$this->uuidVersion}();
+
+        if (! $this->update($episodeId, [
+            'preview_id' => $uuid,
+        ])) {
+            return false;
+        }
+
+        return (string) $uuid;
+    }
+
     /**
      * Gets all episodes for a podcast ordered according to podcast type Filtered depending on year or season
      *
diff --git a/app/Resources/styles/custom.css b/app/Resources/styles/custom.css
index 25883b7427618427ba0e4a71bc826db0b96873ba..6dfa93b1a85be0f5da189e1cc325f115aaf476fb 100644
--- a/app/Resources/styles/custom.css
+++ b/app/Resources/styles/custom.css
@@ -59,7 +59,7 @@
     );
   }
 
-  .bg-stripes-gray {
+  .bg-stripes-default {
     background-image: repeating-linear-gradient(
       -45deg,
       #f3f4f6,
diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php
index 5ed5e3acef257e6d98f03cb839353c24398c5e5c..1a5e57a440d93be5f48bd919a2b86b0033b3f5a8 100644
--- a/modules/Admin/Language/en/Episode.php
+++ b/modules/Admin/Language/en/Episode.php
@@ -22,6 +22,7 @@ return [
     'all_podcast_episodes' => 'All podcast episodes',
     'back_to_podcast' => 'Go back to podcast',
     'edit' => 'Edit',
+    'preview' => 'Preview',
     'publish' => 'Publish',
     'publish_edit' => 'Edit publication',
     'publish_date_edit' => 'Edit publication date',
@@ -211,4 +212,14 @@ return [
         'light' => 'Light',
         'light-transparent' => 'Light transparent',
     ],
+    'publication_status_banner' => [
+        'draft_mode' => 'draft mode',
+        'text' => '{publication_status, select,
+            published {This episode is not yet published.}
+            scheduled {This episode is scheduled for publication on {publication_date}.}
+            with_podcast {This episode will be published at the same time as the podcast.}
+            other {This episode is not yet published.}
+        }',
+        'preview' => 'Preview',
+    ],
 ];
diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php
index bd006e54b37b0652ac7f3c524e3aed3c977d6758..865465fcd1adeba9aafbb948d7da6bbf61cbd928 100644
--- a/themes/cp_admin/_layout.php
+++ b/themes/cp_admin/_layout.php
@@ -67,6 +67,9 @@ $isEpisodeArea = isset($podcast) && isset($episode);
                 <?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?>
             <?php endif; ?>
         <?php endif; ?>
+        <?php if ($isEpisodeArea && $episode->publication_status !== 'published'): ?>
+            <?= episode_publication_status_banner($episode, 'border-b') ?>
+        <?php endif; ?>
         <div class="px-2 py-8 mx-auto md:px-12">
             <?= view('_message_block') ?>
             <?= $this->renderSection('content') ?>
diff --git a/themes/cp_app/episode/_layout-preview.php b/themes/cp_app/episode/_layout-preview.php
new file mode 100644
index 0000000000000000000000000000000000000000..c8caa0368e3c31ccd08ac3e9c09ac5cd84bc7cb5
--- /dev/null
+++ b/themes/cp_app/episode/_layout-preview.php
@@ -0,0 +1,199 @@
+<?= helper('page') ?>
+
+<!DOCTYPE html>
+<html lang="<?= service('request')
+    ->getLocale() ?>">
+
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
+    <link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
+    <link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
+    <meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
+    <script>
+    // Check that service workers are supported
+    if ('serviceWorker' in navigator) {
+        // Use the window load event to keep the page load performant
+        window.addEventListener('load', () => {
+            navigator.serviceWorker.register('/sw.js');
+        });
+    }
+    </script>
+
+    <meta name="robots" content="noindex">
+
+    <title>[<?= lang('Episode.preview.title') ?>] <?= $episode->title ?></title>
+    <meta name="description" content="<?= esc($episode->description) ?>">
+
+    <link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
+    <?= service('vite')
+        ->asset('styles/index.css', 'css') ?>
+    <?= service('vite')
+        ->asset('js/app.ts', 'js') ?>
+    <?= service('vite')
+        ->asset('js/podcast.ts', 'js') ?>
+    <?= service('vite')
+        ->asset('js/audio-player.ts', 'js') ?>
+</head>
+
+<body class="flex flex-col min-h-screen mx-auto md:min-h-full md:grid md:grid-cols-podcast bg-base theme-<?= service('settings')
+        ->get('App.theme') ?>">
+    <?php if (can_user_interact()): ?>
+        <div class="col-span-full">
+            <?= $this->include('_admin_navbar') ?>
+        </div>
+    <?php endif; ?>
+
+    <nav class="flex items-center justify-between h-10 col-start-2 text-white bg-header">
+        <a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
+            'podcast' => esc($podcast->title),
+        ]) ?>">
+            <?= icon('arrow-left', 'text-lg flex-shrink-0') ?>
+            <div class="flex items-center min-w-0 gap-x-2">
+                <img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->cover->tiny_url ?>" alt="<?= esc($episode->podcast->title) ?>" loading="lazy" />
+                <div class="flex flex-col overflow-hidden">
+                    <span class="text-sm font-semibold leading-none truncate"><?= esc($episode->podcast->title) ?></span>
+                    <span class="text-xs"><?= lang('Podcast.followers', [
+                        'numberOfFollowers' => $podcast->actor->followers_count,
+                    ]) ?></span>
+                </div>
+            </div>
+        </a>
+        <div class="inline-flex items-center self-end h-full px-2 gap-x-2">
+            <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
+                <button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><Icon glyph="heart"></Icon></button>
+            <?php endif; ?>
+            <?= anchor_popup(
+                route_to('follow', esc($podcast->handle)),
+                icon(
+                    'social/castopod',
+                    'mr-2 text-xl text-black/75 group-hover:text-black',
+                ) . lang('Podcast.follow'),
+                [
+                    'width'  => 420,
+                    'height' => 620,
+                    'class'  => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow focus:ring-accent bg-white',
+                ],
+            ) ?>
+        </div>
+    </nav>
+    <header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-accent-base/75 gap-y-4">
+        <div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= get_podcast_banner_url($episode->podcast, 'small') ?>');"></div>
+        <div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div>
+        <div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row">
+            <div class="relative flex-shrink-0">
+                <?= explicit_badge($episode->parental_advisory === 'explicit', 'rounded absolute left-0 bottom-0 ml-2 mb-2 bg-black/75 text-accent-contrast') ?>
+                <?php if ($episode->is_premium): ?>
+                    <Icon glyph="exchange-dollar" class="absolute left-0 w-8 pl-2 text-2xl rounded-r-full rounded-tl-lg top-2 text-accent-contrast bg-accent-base" />
+                <?php endif; ?>
+                <img src="<?= $episode->cover->medium_url ?>" alt="<?= esc($episode->title) ?>" class="flex-shrink-0 rounded-md shadow-xl h-36 aspect-square" loading="lazy" />
+            </div>
+            <div class="flex flex-col items-start w-full min-w-0 text-white">
+                <?= episode_numbering($episode->number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?>
+                <h1 class="inline-flex items-baseline max-w-lg mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></h1>
+                <div class="flex items-center w-full mt-4 gap-x-8">
+                <?php if ($episode->persons !== []): ?>
+                    <button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden">
+                        <span class="inline-flex flex-row-reverse">
+                            <?php $i = 0; ?>
+                            <?php foreach ($episode->persons as $person): ?>
+                                <img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
+                                <?php $i++;
+                                if ($i === 3) {
+                                    break;
+                                }?>
+                            <?php endforeach; ?>
+                        </span>
+                        <?= lang('Episode.persons', [
+                            'personsCount' => count($episode->persons),
+                        ]) ?>
+                    </button>
+                <?php endif; ?>
+                <?php if ($episode->location): ?>
+                    <?= location_link($episode->location, 'text-xs font-semibold p-2') ?>
+                <?php endif; ?>
+                </div>
+            </div>
+        </div>
+        <div class="z-10 inline-flex items-center text-white gap-x-4">
+            <play-episode-button
+                id="<?= $episode->id ?>"
+                imageSrc="<?= $episode->cover->thumbnail_url ?>"
+                title="<?= esc($episode->title) ?>"
+                podcast="<?= esc($episode->podcast->title) ?>"
+                src="<?= $episode->audio->file_url ?>"
+                mediaType="<?= $episode->audio->file_mimetype ?>"
+                playLabel="<?= lang('Common.play_episode_button.play') ?>"
+                playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button>
+            <div class="text-xs">
+                <?php if ($episode->published_at):  ?>
+                    <?= relative_time($episode->published_at) ?>
+                <?php else: ?>
+                    <?= lang('Episode.preview.not_published') ?>
+                <?php endif; ?>
+                <span class="mx-1">•</span>
+                <time datetime="PT<?= round($episode->audio->duration, 3) ?>S">
+                    <?= format_duration_symbol((int) $episode->audio->duration) ?>
+                </time>
+            </div>
+        </div>
+    </header>
+    <div class="col-start-2 px-8 py-4 text-white bg-header">
+        <h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
+        <?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
+            <SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore>
+        <?php else: ?>
+            <div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div>
+        <?php endif; ?>
+    </div>
+    <?= $this->include('episode/_partials/navigation') ?>
+    <?= $this->include('podcast/_partials/premium_banner') ?>
+    <div class="flex flex-wrap items-center min-h-[2.5rem] col-start-2 p-1 mt-2 md:mt-4 rounded-conditional-full gap-y-2 sm:flex-row bg-accent-base text-accent-contrast" role="alert">
+        <div class="flex flex-wrap gap-4 pl-2">
+            <div class="inline-flex items-center gap-2 font-semibold tracking-wide uppercase">
+                <Icon glyph="eye" />
+                <span class="text-xs"><?= lang('Episode.preview.title') ?></span>
+            </div>
+            <p class="text-sm">
+                <?= lang('Episode.preview.text', [
+                    'publication_status' => $episode->publication_status,
+                    'publication_date'   => $episode->published_at ? local_datetime($episode->published_at) : null,
+                ], null, false); ?>
+            </p>
+        </div>
+        <?php if (auth()->loggedIn()): ?>
+            <?php if (in_array($episode->publication_status, ['scheduled', 'with_podcast'], true)): ?>
+                <Button
+                    iconLeft="upload-cloud"
+                    variant="primary"
+                    size="small"
+                    class="ml-auto"
+                    uri="<?= route_to('episode-publish_edit', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish_edit') ?></Button>
+            <?php else: ?>
+                <Button
+                    iconLeft="upload-cloud"
+                    variant="secondary"
+                    size="small"
+                    class="ml-auto"
+                    uri="<?= route_to('episode-publish', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish') ?></Button>
+            <?php endif; ?>
+        <?php endif; ?>
+    </div>
+    <div class="relative grid items-start flex-1 col-start-2 grid-cols-podcastMain gap-x-6">
+        <main class="w-full col-start-1 row-start-1 py-6 col-span-full md:col-span-1">
+            <?= $this->renderSection('content') ?>
+        </main>
+        <div data-sidebar-toggler="backdrop" class="absolute top-0 left-0 z-10 hidden w-full h-full bg-backdrop/75 md:hidden" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>"></div>
+        <?= $this->include('podcast/_partials/sidebar') ?>
+    </div>
+    <?= view('_persons_modal', [
+        'title' => lang('Episode.persons_list', [
+            'episodeTitle' => esc($episode->title),
+        ]),
+        'persons' => $episode->persons,
+    ]) ?>
+    <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
+        <?= $this->include('podcast/_partials/funding_links_modal') ?>
+    <?php endif; ?>
+</body>
diff --git a/themes/cp_app/episode/_partials/navigation.php b/themes/cp_app/episode/_partials/navigation.php
index ddeedac4bf8ae3d0fd29ebf93d166235d89b4a3e..73fdcf5b8d42f841972b3c9e721a014b507e35dc 100644
--- a/themes/cp_app/episode/_partials/navigation.php
+++ b/themes/cp_app/episode/_partials/navigation.php
@@ -1,17 +1,32 @@
 <?php declare(strict_types=1);
 
-$navigationItems = [
-    [
-        'uri'       => route_to('episode', esc($podcast->handle), esc($episode->slug)),
-        'label'     => lang('Episode.comments'),
-        'labelInfo' => $episode->comments_count,
-    ],
-    [
-        'uri'       => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)),
-        'label'     => lang('Episode.activity'),
-        'labelInfo' => $episode->posts_count,
-    ],
-]
+if ($episode->publication_status === 'published') {
+    $navigationItems = [
+        [
+            'uri'       => route_to('episode', esc($podcast->handle), esc($episode->slug)),
+            'label'     => lang('Episode.comments'),
+            'labelInfo' => $episode->comments_count,
+        ],
+        [
+            'uri'       => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)),
+            'label'     => lang('Episode.activity'),
+            'labelInfo' => $episode->posts_count,
+        ],
+    ];
+} else {
+    $navigationItems = [
+        [
+            'uri'       => route_to('episode-preview', $episode->preview_id),
+            'label'     => lang('Episode.comments'),
+            'labelInfo' => $episode->comments_count,
+        ],
+        [
+            'uri'       => route_to('episode-preview-activity', $episode->preview_id),
+            'label'     => lang('Episode.activity'),
+            'labelInfo' => $episode->posts_count,
+        ],
+    ];
+}
 ?>
 <nav class="sticky z-40 flex col-start-2 pt-4 shadow bg-elevated md:px-8 gap-x-2 md:gap-x-4 -top-4 rounded-conditional-b-xl">
     <?php foreach ($navigationItems as $item): ?>
diff --git a/themes/cp_app/episode/preview-activity.php b/themes/cp_app/episode/preview-activity.php
new file mode 100644
index 0000000000000000000000000000000000000000..31320151519145b7ea8bdc5ba32787bba30c6c70
--- /dev/null
+++ b/themes/cp_app/episode/preview-activity.php
@@ -0,0 +1,15 @@
+<?= $this->extend('episode/_layout-preview') ?>
+
+<?= $this->section('content') ?>
+
+<div class="flex flex-col gap-y-4">
+    <?php foreach ($episode->posts as $key => $post): ?>
+        <?= view('post/_partials/card', [
+    'index'           => $key,
+            'post'    => $post,
+            'podcast' => $podcast,
+]) ?>
+    <?php endforeach; ?>
+</div>
+
+<?= $this->endSection() ?>
diff --git a/themes/cp_app/episode/preview-comments.php b/themes/cp_app/episode/preview-comments.php
new file mode 100644
index 0000000000000000000000000000000000000000..33925b23d43268d7af894d9d399724efca86f98c
--- /dev/null
+++ b/themes/cp_app/episode/preview-comments.php
@@ -0,0 +1,14 @@
+<?= $this->extend('episode/_layout-preview') ?>
+
+<?= $this->section('content') ?>
+
+<div class="flex flex-col gap-y-2">
+    <?php foreach ($episode->comments as $comment): ?>
+        <?= view('episode/_partials/comment', [
+    'comment'         => $comment,
+            'podcast' => $podcast,
+]) ?>
+    <?php endforeach; ?>
+</div>
+
+<?= $this->endSection() ?>
\ No newline at end of file