diff --git a/app/Config/App.php b/app/Config/App.php
index 2c5e100ff852ac25ea50eb98cd25f5267fbce424..d547394793470dd190374e76341497f0d1996b2a 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -435,6 +435,8 @@ class App extends BaseConfig
      */
     public string $siteName = 'Castopod';
 
+    public string $siteTitleSeparator = ' | ';
+
     public string $siteDescription = 'Castopod Host is an open-source hosting platform made for podcasters who want engage and interact with their audience.';
 
     /**
diff --git a/app/Config/Embed.php b/app/Config/Embed.php
new file mode 100644
index 0000000000000000000000000000000000000000..0c97f86f08b57e7a2286662641a7c30ea77dff7d
--- /dev/null
+++ b/app/Config/Embed.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Embed extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Embeddable player config
+     * --------------------------------------------------------------------------
+     */
+    public int $width = 600;
+
+    public int $height = 144;
+}
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 7d2ca19f731c82e43055b9850624a0a6a5ff9344..161c02fa4abc20a73312e7e1f92e48b337c46976 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -180,10 +180,10 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
 $routes->get('/credits', 'CreditsController', [
     'as' => 'credits',
 ]);
-$routes->get('/map', 'MapMarkerController', [
+$routes->get('/map', 'MapController', [
     'as' => 'map',
 ]);
-$routes->get('/episodes-markers', 'MapMarkerController::getEpisodesMarkers', [
+$routes->get('/episodes-markers', 'MapController::getEpisodesMarkers', [
     'as' => 'episodes-markers',
 ]);
 $routes->get('/pages/(:slug)', 'PageController/$1', [
diff --git a/app/Controllers/ActorController.php b/app/Controllers/ActorController.php
index ff1dbae54b5fad448d46f939d60c77481dad3dbd..b7233df4decfa05880c0879454455c73d17bf507 100644
--- a/app/Controllers/ActorController.php
+++ b/app/Controllers/ActorController.php
@@ -20,7 +20,7 @@ class ActorController extends FediverseActorController
     /**
      * @var string[]
      */
-    protected $helpers = ['auth', 'svg', 'components', 'misc'];
+    protected $helpers = ['auth', 'svg', 'components', 'misc', 'seo'];
 
     public function follow(): string
     {
@@ -34,6 +34,8 @@ class ActorController extends FediverseActorController
         if (! ($cachedView = cache($cacheName))) {
             helper(['form', 'components', 'svg']);
             $data = [
+                // @phpstan-ignore-next-line
+                'metatags' => get_follow_metatags($this->actor),
                 'actor' => $this->actor,
             ];
 
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 2d30eb3c5602994967bd63f42de26d75cd23fb63..312f1ec2fbe3af78622b9673faff7d5f5dbe7025 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -28,7 +28,7 @@ class BaseController extends Controller
         ResponseInterface $response,
         LoggerInterface $logger
     ): void {
-        $this->helpers = array_merge($this->helpers, ['auth', 'svg', 'components', 'misc']);
+        $this->helpers = array_merge($this->helpers, ['auth', 'svg', 'components', 'misc', 'seo']);
 
         // Do Not Edit This Line
         parent::initController($request, $response, $logger);
diff --git a/app/Controllers/CreditsController.php b/app/Controllers/CreditsController.php
index 816653dd15006ec31fa9e9823a5a634add92a0c1..3531b3e2e86e8c230426a03ae1895564c4f5ee8a 100644
--- a/app/Controllers/CreditsController.php
+++ b/app/Controllers/CreditsController.php
@@ -165,11 +165,12 @@ class CreditsController extends BaseController
             }
 
             $data = [
+                'metatags' => get_page_metatags($page),
                 'page' => $page,
                 'credits' => $credits,
             ];
 
-            $found = view('credits', $data);
+            $found = view('pages/credits', $data);
 
             cache()
                 ->save($cacheName, $found, DECADE);
diff --git a/app/Controllers/EpisodeCommentController.php b/app/Controllers/EpisodeCommentController.php
index f64c1db30428f4514c9f7ce0e827133df6188577..6bb1ef0e67909595d7e9eb52fc7d2ab743e40fa2 100644
--- a/app/Controllers/EpisodeCommentController.php
+++ b/app/Controllers/EpisodeCommentController.php
@@ -95,6 +95,7 @@ class EpisodeCommentController extends BaseController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                'metatags' => get_episode_comment_metatags($this->comment),
                 'podcast' => $this->podcast,
                 'actor' => $this->actor,
                 'episode' => $this->episode,
diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php
index 3a59f91a2788785875176dd89d608147091304c8..e79b9be5f0f8ab4a1c25a803de5495add274efc0 100644
--- a/app/Controllers/EpisodeController.php
+++ b/app/Controllers/EpisodeController.php
@@ -77,6 +77,7 @@ class EpisodeController extends BaseController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                'metatags' => get_episode_metatags($this->episode),
                 'podcast' => $this->podcast,
                 'episode' => $this->episode,
             ];
@@ -115,6 +116,7 @@ class EpisodeController extends BaseController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                'metatags' => get_episode_metatags($this->episode),
                 'podcast' => $this->podcast,
                 'episode' => $this->episode,
             ];
@@ -220,20 +222,21 @@ class EpisodeController extends BaseController
         $oembed->addChild('author_name', $this->podcast->title);
         $oembed->addChild('author_url', $this->podcast->link);
         $oembed->addChild('thumbnail', $this->episode->cover->large_url);
-        $oembed->addChild('thumbnail_width', config('Images')->podcastCoverSizes['large'][0]);
-        $oembed->addChild('thumbnail_height', config('Images')->podcastCoverSizes['large'][1]);
+        $oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['large'][0]);
+        $oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['large'][1]);
         $oembed->addChild(
             'html',
             htmlentities(
                 '<iframe src="' .
                     $this->episode->embed_url .
-                    '" width="100%" height="144" frameborder="0" scrolling="no"></iframe>',
+                    '" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
             ),
         );
-        $oembed->addChild('width', '600');
-        $oembed->addChild('height', '144');
+        $oembed->addChild('width', (string) config('Embed')->width);
+        $oembed->addChild('height', (string) config('Embed')->height);
 
-        return $this->response->setXML((string) $oembed);
+        // @phpstan-ignore-next-line
+        return $this->response->setXML($oembed);
     }
 
     /**
diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php
index f19eb74d1e976bd1d11b7822549184bad7e15bb2..aeaaff0ab76745ae8640b2ba8a907a7e80a5a0e2 100644
--- a/app/Controllers/HomeController.php
+++ b/app/Controllers/HomeController.php
@@ -36,6 +36,7 @@ class HomeController extends BaseController
 
         // default behavior: list all podcasts on home page
         $data = [
+            'metatags' => get_home_metatags(),
             'podcasts' => $allPodcasts,
         ];
 
diff --git a/app/Controllers/MapMarkerController.php b/app/Controllers/MapController.php
similarity index 95%
rename from app/Controllers/MapMarkerController.php
rename to app/Controllers/MapController.php
index 5ead95979baab6bca5533631523a1641ebcbaccb..c4a6cd4bde29f23f39d11e9d332a8a058c395c31 100644
--- a/app/Controllers/MapMarkerController.php
+++ b/app/Controllers/MapController.php
@@ -13,7 +13,7 @@ namespace App\Controllers;
 use App\Models\EpisodeModel;
 use CodeIgniter\HTTP\ResponseInterface;
 
-class MapMarkerController extends BaseController
+class MapController extends BaseController
 {
     public function index(): string
     {
@@ -21,7 +21,7 @@ class MapMarkerController extends BaseController
             ->getLocale();
         $cacheName = "page_map_{$locale}";
         if (! ($found = cache($cacheName))) {
-            $found = view('map', [], [
+            $found = view('pages/map', [], [
                 'cache' => DECADE,
                 'cache_name' => $cacheName,
             ]);
diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php
index f1e1bf2a79966462c795f2d712bb7a3ae63b168b..3a251a7dca7e5fc00078fd83a34d7ab1ccf5f730 100644
--- a/app/Controllers/PageController.php
+++ b/app/Controllers/PageController.php
@@ -40,10 +40,11 @@ class PageController extends BaseController
         $cacheName = "page-{$this->page->slug}";
         if (! ($found = cache($cacheName))) {
             $data = [
+                'metatags' => get_page_metatags($this->page),
                 'page' => $this->page,
             ];
 
-            $found = view('page', $data);
+            $found = view('pages/page', $data);
 
             // The page cache is set to a decade so it is deleted manually upon page update
             cache()
diff --git a/app/Controllers/PodcastController.php b/app/Controllers/PodcastController.php
index f330064d436f8e4861d1a422e5dbe21f95e6f84f..6d0b6c690e4349db0dde5f524e7b3cbb517c0da1 100644
--- a/app/Controllers/PodcastController.php
+++ b/app/Controllers/PodcastController.php
@@ -80,6 +80,7 @@ class PodcastController extends BaseController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                'metatags' => get_podcast_metatags($this->podcast, 'activity'),
                 'podcast' => $this->podcast,
                 'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
             ];
@@ -125,6 +126,7 @@ class PodcastController extends BaseController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                'metatags' => get_podcast_metatags($this->podcast, 'about'),
                 'podcast' => $this->podcast,
             ];
 
@@ -240,6 +242,7 @@ class PodcastController extends BaseController
             }
 
             $data = [
+                'metatags' => get_podcast_metatags($this->podcast, 'episodes'),
                 'podcast' => $this->podcast,
                 'episodesNav' => $episodesNavigation,
                 'activeQuery' => $activeQuery,
diff --git a/app/Controllers/PostController.php b/app/Controllers/PostController.php
index 69837feb0d369e143df1502f8af8e0a3eaabd4f5..0edb02dca51d6402194b448c8744e7b08148a842 100644
--- a/app/Controllers/PostController.php
+++ b/app/Controllers/PostController.php
@@ -35,7 +35,7 @@ class PostController extends FediversePostController
     /**
      * @var string[]
      */
-    protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc'];
+    protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo'];
 
     public function _remap(string $method, string ...$params): mixed
     {
@@ -81,6 +81,8 @@ class PostController extends FediversePostController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                // @phpstan-ignore-next-line
+                'metatags' => get_post_metatags($this->post),
                 'post' => $this->post,
                 'podcast' => $this->podcast,
             ];
@@ -233,6 +235,8 @@ class PostController extends FediversePostController
 
         if (! ($cachedView = cache($cacheName))) {
             $data = [
+                // @phpstan-ignore-next-line
+                'metatags' => get_remote_actions_metatags($this->post, $action),
                 'podcast' => $this->podcast,
                 'actor' => $this->actor,
                 'post' => $this->post,
diff --git a/app/Helpers/page_helper.php b/app/Helpers/page_helper.php
index beb1eb27b7a6914081cf78febd60151d31446785..7817a4d9398cc72fa140438c735625c22d0b8c48 100644
--- a/app/Helpers/page_helper.php
+++ b/app/Helpers/page_helper.php
@@ -25,7 +25,7 @@ if (! function_exists('render_page_links')) {
         $links .= anchor(route_to('credits'), lang('Person.credits'), [
             'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
         ]);
-        $links .= anchor(route_to('map'), lang('Page.map'), [
+        $links .= anchor(route_to('map'), lang('Page.map.title'), [
             'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
         ]);
         foreach ($pages as $page) {
diff --git a/app/Helpers/seo_helper.php b/app/Helpers/seo_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..36edf1cb2c883f703156af5ceb999180532ef84a
--- /dev/null
+++ b/app/Helpers/seo_helper.php
@@ -0,0 +1,275 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Entities\Actor;
+use App\Entities\Episode;
+use App\Entities\EpisodeComment;
+use App\Entities\Page;
+use App\Entities\Podcast;
+use App\Entities\Post;
+use Melbahja\Seo\MetaTags;
+use Melbahja\Seo\Schema;
+use Melbahja\Seo\Schema\Thing;
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+if (! function_exists('get_podcast_metatags')) {
+    function get_podcast_metatags(Podcast $podcast, string $page): string
+    {
+        $schema = new Schema(
+            new Thing('PodcastSeries', [
+                'name' => $podcast->title,
+                'url' => url_to('podcast-activity', $podcast->handle),
+                'image' => $podcast->cover->feed_url,
+                'description' => $podcast->description,
+                'webFeed' => $podcast->feed_url,
+                'author' => new Thing('Person', [
+                    'name' => $podcast->publisher,
+                ]),
+            ])
+        );
+
+        $metatags = new MetaTags();
+
+        $metatags
+            ->title('  ' . $podcast->title . " (@{$podcast->handle})" . ' • ' . lang('Podcast.' . $page))
+            ->description(htmlspecialchars($podcast->description))
+            ->image((string) $podcast->cover->large_url)
+            ->canonical((string) current_url())
+            ->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0])
+            ->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1])
+            ->og('locale', $podcast->language_code)
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        if ($podcast->payment_pointer) {
+            $metatags->meta('monetization', $podcast->payment_pointer);
+        }
+
+        return '<link type="application/rss+xml" rel="alternate" title="' . $podcast->title . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString();
+    }
+}
+
+if (! function_exists('get_episode_metatags')) {
+    function get_episode_metatags(Episode $episode): string
+    {
+        $schema = new Schema(
+            new Thing('PodcastEpisode', [
+                'url' => url_to('episode', $episode->podcast->handle, $episode->slug),
+                'name' => $episode->title,
+                'image' => $episode->cover->feed_url,
+                'description' => $episode->description,
+                'datePublished' => $episode->published_at->format(DATE_ISO8601),
+                'timeRequired' => iso8601_duration($episode->audio_file_duration),
+                'associatedMedia' => new Thing('MediaObject', [
+                    'contentUrl' => $episode->audio_file_url,
+                ]),
+                'partOfSeries' => new Thing('PodcastSeries', [
+                    'name' => $episode->podcast->title,
+                    'url' => url_to('podcast-activity', $episode->podcast->handle),
+                ]),
+            ])
+        );
+
+        $metatags = new MetaTags();
+
+        $metatags
+            ->title($episode->title)
+            ->description(htmlspecialchars($episode->description))
+            ->image((string) $episode->cover->large_url, 'player')
+            ->canonical($episode->link)
+            ->og('site_name', service('settings')->get('App.siteName'))
+            ->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0])
+            ->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1])
+            ->og('locale', $episode->podcast->language_code)
+            ->og('audio', $episode->audio_file_opengraph_url)
+            ->og('audio:type', $episode->audio_file_mimetype)
+            ->meta('article:published_time', $episode->published_at->format(DATE_ISO8601))
+            ->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601))
+            ->twitter('audio:partner', $episode->podcast->publisher ?? '')
+            ->twitter('audio:artist_name', $episode->podcast->owner_name)
+            ->twitter('player', $episode->getEmbedUrl('light'))
+            ->twitter('player:width', (string) config('Embed')->width)
+            ->twitter('player:height', (string) config('Embed')->height);
+
+        if ($episode->podcast->payment_pointer) {
+            $metatags->meta('monetization', $episode->podcast->payment_pointer);
+        }
+
+        return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
+            route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
+        ) . '" title="' . $episode->title . ' oEmbed json" />' . PHP_EOL . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
+            route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug)
+        ) . '" title="' . $episode->title . ' oEmbed xml" />' . PHP_EOL . $schema->__toString();
+    }
+}
+
+if (! function_exists('get_post_metatags')) {
+    function get_post_metatags(Post $post): string
+    {
+        $socialMediaPosting = new Thing('SocialMediaPosting', [
+            '@id' => url_to('post', $post->actor->username, $post->id),
+            'datePublished' => $post->published_at->format(DATE_ISO8601),
+            'author' => new Thing('Person', [
+                'name' => $post->actor->display_name,
+                'url' => $post->actor->uri,
+            ]),
+            'text' => $post->message,
+        ]);
+
+        if ($post->episode_id !== null) {
+            $socialMediaPosting->__set('sharedContent', new Thing('Audio', [
+                'headline' => $post->episode->title,
+                'url' => $post->episode->link,
+                'author' => new Thing('Person', [
+                    'name' => $post->episode->podcast->owner_name,
+                ]),
+            ]));
+        } elseif ($post->preview_card !== null) {
+            $socialMediaPosting->__set('sharedContent', new Thing('WebPage', [
+                'headline' => $post->preview_card->title,
+                'url' => $post->preview_card->url,
+                'author' => new Thing('Person', [
+                    'name' => $post->preview_card->author_name,
+                ]),
+            ]));
+        }
+
+        $schema = new Schema($socialMediaPosting);
+
+        $metatags = new MetaTags();
+        $metatags
+            ->title(lang('Post.title', [
+                'actorDisplayName' => $post->actor->display_name,
+            ]))
+            ->description($post->message)
+            ->image($post->actor->avatar_image_url)
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString() . PHP_EOL . $schema->__toString();
+    }
+}
+
+if (! function_exists('get_episode_comment_metatags')) {
+    function get_episode_comment_metatags(EpisodeComment $episodeComment): string
+    {
+        $schema = new Schema(new Thing('SocialMediaPosting', [
+            '@id' => url_to(
+                'episode-comment',
+                $episodeComment->actor->username,
+                $episodeComment->episode->slug,
+                $episodeComment->id
+            ),
+            'datePublished' => $episodeComment->created_at->format(DATE_ISO8601),
+            'author' => new Thing('Person', [
+                'name' => $episodeComment->actor->display_name,
+                'url' => $episodeComment->actor->uri,
+            ]),
+            'text' => $episodeComment->message,
+            'upvoteCount' => $episodeComment->likes_count,
+        ]));
+
+        $metatags = new MetaTags();
+        $metatags
+            ->title(lang('Comment.title', [
+                'actorDisplayName' => $episodeComment->actor->display_name,
+                'episodeTitle' => $episodeComment->episode->title,
+            ]))
+            ->description($episodeComment->message)
+            ->image($episodeComment->actor->avatar_image_url)
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString() . PHP_EOL . $schema->__toString();
+    }
+}
+
+if (! function_exists('get_follow_metatags')) {
+    function get_follow_metatags(Actor $actor): string
+    {
+        $metatags = new MetaTags();
+        $metatags
+            ->title(lang('Podcast.followTitle', [
+                'actorDisplayName' => $actor->display_name,
+            ]))
+            ->description($actor->summary)
+            ->image($actor->avatar_image_url)
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString();
+    }
+}
+
+if (! function_exists('get_remote_actions_metatags')) {
+    function get_remote_actions_metatags(Post $post, string $action): string
+    {
+        $metatags = new MetaTags();
+        $metatags
+            ->title(lang('Fediverse.' . $action . '.title', [
+                'actorDisplayName' => $post->actor->display_name,
+            ],))
+            ->description($post->message)
+            ->image($post->actor->avatar_image_url)
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString();
+    }
+}
+
+if (! function_exists('get_home_metatags')) {
+    function get_home_metatags(): string
+    {
+        $metatags = new MetaTags();
+        $metatags
+            ->title(service('settings')->get('App.siteName'))
+            ->description(service('settings')->get('App.siteDescription'))
+            ->image(service('settings')->get('App.siteIcon')['512'])
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString();
+    }
+}
+
+if (! function_exists('get_page_metatags')) {
+    function get_page_metatags(Page $page): string
+    {
+        $metatags = new MetaTags();
+        $metatags
+            ->title(
+                $page->title . service('settings')->get('App.siteTitleSeparator') . service(
+                    'settings'
+                )->get('App.siteName')
+            )
+            ->description(service('settings')->get('App.siteDescription'))
+            ->image(service('settings')->get('App.siteIcon')['512'])
+            ->canonical((string) current_url())
+            ->og('site_name', service('settings')->get('App.siteName'));
+
+        return $metatags->__toString();
+    }
+}
+
+if (! function_exists('iso8601_duration')) {
+    // From https://stackoverflow.com/a/40761380
+    function iso8601_duration(float $seconds): string
+    {
+        $days = floor($seconds / 86400);
+        $seconds %= 86400;
+
+        $hours = floor($seconds / 3600);
+        $seconds %= 3600;
+
+        $minutes = floor($seconds / 60);
+        $seconds %= 60;
+
+        return sprintf('P%dDT%dH%dM%dS', $days, $hours, $minutes, $seconds);
+    }
+}
diff --git a/app/Language/en/Page.php b/app/Language/en/Page.php
index 592b87846447755a83bdd76c16c0abcc67105313..9a255bda06546fb8d683d1d7f7f5cb9fecdead92 100644
--- a/app/Language/en/Page.php
+++ b/app/Language/en/Page.php
@@ -10,21 +10,8 @@ declare(strict_types=1);
 
 return [
     'back_to_home' => 'Back to home',
-    'page' => 'Page',
-    'all_pages' => 'All pages',
-    'create' => 'New page',
-    'go_to_page' => 'Go to page',
-    'edit' => 'Edit page',
-    'delete' => 'Delete page',
-    'form' => [
-        'title' => 'Title',
-        'permalink' => 'Permalink',
-        'content' => 'Content',
-        'submit_create' => 'Create page',
-        'submit_edit' => 'Save',
+    'map' => [
+        'title' => 'Map',
+        'description' => 'Discover podcast episodes on {siteName} that are placed on a map! Travel through the map and listen to episodes that talk about specific locations.',
     ],
-    'messages' => [
-        'createSuccess' => 'The page “{pageTitle}” was created successfully!',
-    ],
-    'map' => 'Map',
 ];
diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php
index d8c0a20e483d7e67fc03ab8142e42d50426f65df..7528a9a3e2f4801db6ee2f4b1eea2110c3450113 100644
--- a/app/Language/en/Podcast.php
+++ b/app/Language/en/Podcast.php
@@ -28,8 +28,11 @@ return [
         other {<span class="font-semibold">#</span> posts}
     }',
     'activity' => 'Activity',
+    'activity_title' => '{podcastTitle} news & activity',
     'episodes' => 'Episodes',
+    'episodes_title' => 'Episodes of {podcastTitle}',
     'about' => 'About',
+    'about_title' => 'About {podcastTitle}',
     'sponsor_title' => 'Enjoying the show?',
     'sponsor' => 'Sponsor',
     'funding_links' => 'Funding links for {podcastTitle}',
diff --git a/app/Language/fr/Page.php b/app/Language/fr/Page.php
index 220b1a73efface001656efd6caff150b85d49969..8ad3f5d1134dfee336c0ebc5e6e1263c3919b1b9 100644
--- a/app/Language/fr/Page.php
+++ b/app/Language/fr/Page.php
@@ -10,21 +10,8 @@ declare(strict_types=1);
 
 return [
     'back_to_home' => 'Retour à l’accueil',
-    'page' => 'Page',
-    'all_pages' => 'Toutes les pages',
-    'create' => 'Créer une page',
-    'go_to_page' => 'Aller à la page',
-    'edit' => 'Modifier la page',
-    'delete' => 'Supprimer la page',
-    'form' => [
-        'title' => 'Titre',
-        'permalink' => 'Lien permanent',
-        'content' => 'Contenu',
-        'submit_create' => 'Créer la page',
-        'submit_edit' => 'Enregistrer',
+    'map' => [
+        'title' => 'Cartographie',
+        'description' => 'Découvrez des épisodes de podcast placés sur une carte avec {siteName} ! Voyagez sur une carte du monde et écoutez des épisodes mentionnant des lieux spécifiques.',
     ],
-    'messages' => [
-        'createSuccess' => 'La page {pageTitle} a été créée avec succès !',
-    ],
-    'map' => 'Cartographie',
 ];
diff --git a/composer.json b/composer.json
index b7dffcaf402d6e2ca6017f1da7ef98e4d48d66fe..a2f505883c9ade4225c7cb5f4ca79e6e54e866a5 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,8 @@
     "michalsn/codeigniter4-uuid": "dev-develop",
     "essence/essence": "^3.5.4",
     "codeigniter4/settings": "dev-develop",
-    "chrisjean/php-ico": "^1.0"
+    "chrisjean/php-ico": "^1.0",
+    "melbahja/seo": "^2.0"
   },
   "require-dev": {
     "mikey179/vfsstream": "^v1.6.8",
diff --git a/composer.lock b/composer.lock
index 82b1287a717d7bc55e249105cad9a44e797c9881..fabdde839b8b8d37b9f6b011336fc8108e1da711 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
     "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
     "This file is @generated automatically"
   ],
-  "content-hash": "f35a050323bdc632cd550f9d13f0679c",
+  "content-hash": "c0a25c3d11c806b4bc62eafb22902bc8",
   "packages": [
     {
       "name": "brick/math",
@@ -1120,6 +1120,64 @@
       },
       "time": "2020-11-02T17:00:53+00:00"
     },
+    {
+      "name": "melbahja/seo",
+      "version": "v2.0.0",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/melbahja/seo.git",
+        "reference": "a42500223cb532d4069e85097cc5b5e6ee402de1"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/melbahja/seo/zipball/a42500223cb532d4069e85097cc5b5e6ee402de1",
+        "reference": "a42500223cb532d4069e85097cc5b5e6ee402de1",
+        "shasum": ""
+      },
+      "require": {
+        "ext-curl": "*",
+        "ext-xml": "*",
+        "php": ">=7.1"
+      },
+      "require-dev": {
+        "phpunit/phpunit": "^8.5"
+      },
+      "type": "library",
+      "autoload": {
+        "psr-4": {
+          "Melbahja\\Seo\\": "src/"
+        }
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["MIT"],
+      "authors": [
+        {
+          "name": "Mohamed ELbahja",
+          "email": "mohamed@elbahja.me",
+          "homepage": "https://elbahja.me",
+          "role": "Developer"
+        }
+      ],
+      "description": "Simple PHP library to help developers 🍻 do better on-page SEO optimization",
+      "keywords": [
+        "PHP7",
+        "meta tags",
+        "open graph",
+        "php7.1",
+        "schema.org",
+        "search engine optimization",
+        "seo",
+        "sitemap index",
+        "sitemap.xml",
+        "sitemaps",
+        "twitter tags"
+      ],
+      "support": {
+        "issues": "https://github.com/melbahja/seo/issues",
+        "source": "https://github.com/melbahja/seo/tree/v2.0.0"
+      },
+      "time": "2021-10-26T00:36:49+00:00"
+    },
     {
       "name": "michalsn/codeigniter4-uuid",
       "version": "dev-develop",
diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php
index 11e602136aa98f56209c263ef9b3fa4ed42fbdca..9cc6cdc062c98fbdef63ccd3cbd226a4baccb7f8 100644
--- a/themes/cp_admin/_layout.php
+++ b/themes/cp_admin/_layout.php
@@ -4,6 +4,8 @@
 
 <head>
     <meta charset="UTF-8"/>
+    <meta name="robots" content="noindex">
+
     <title><?= $this->renderSection('title') ?> | Castopod Admin</title>
     <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
diff --git a/themes/cp_admin/episode/_sidebar.php b/themes/cp_admin/episode/_sidebar.php
index 96d21e343f95181cc9201cc24ff27d10e1273036..af4c2e3bfa3f27fc9a2a5771505687fdb7032ac9 100644
--- a/themes/cp_admin/episode/_sidebar.php
+++ b/themes/cp_admin/episode/_sidebar.php
@@ -16,7 +16,7 @@ $podcastNavigation = [
     />
     <span class="flex-1 w-full px-2 text-xs font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span>
 </a>
-<div class="flex items-center px-4 py-2 border-t border-b border-navigation">
+<div class="flex items-center px-4 py-2 border-y border-navigation">
     <img
     src="<?= $episode->cover->thumbnail_url ?>"
     alt="<?= $episode->title ?>"
diff --git a/themes/cp_admin/episode/publish.php b/themes/cp_admin/episode/publish.php
index 692483e4bc1d319eca9cc286550853ace6b7750f..a0397e536f3cb383923e053501f4ddc57677d869 100644
--- a/themes/cp_admin/episode/publish.php
+++ b/themes/cp_admin/episode/publish.php
@@ -39,7 +39,7 @@
     <div class="px-4 mb-2">
         <Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
     </div>
-    <div class="flex border-t border-b">
+    <div class="flex border-y">
         <img src="<?= $episode->cover
     ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" />
         <div class="flex flex-col flex-1">
diff --git a/themes/cp_admin/episode/publish_edit.php b/themes/cp_admin/episode/publish_edit.php
index 17f0162cc83ef7d83a368951b55e18ead0e90649..6e38016e91358e8013c544b80d2209d7d2025615 100644
--- a/themes/cp_admin/episode/publish_edit.php
+++ b/themes/cp_admin/episode/publish_edit.php
@@ -41,7 +41,7 @@
     <div class="px-4 mb-2">
         <Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= $post->message ?>" rows="2" />
     </div>
-    <div class="flex border-t border-b">
+    <div class="flex border-y">
         <img src="<?= $episode->cover
                 ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" />
         <div class="flex flex-col flex-1">
diff --git a/themes/cp_app/embed.php b/themes/cp_app/embed.php
index 0f4b3464730ef4046dc7fce23ae481dfb7fd7cb9..e81ac9e962e1a83771e237b6282d03135e560c4a 100644
--- a/themes/cp_app/embed.php
+++ b/themes/cp_app/embed.php
@@ -4,6 +4,7 @@
 
 <head>
     <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <title><?= $episode->title ?></title>
     <meta name="description" content="<?= htmlspecialchars(
         $episode->description,
diff --git a/themes/cp_app/episode/_layout.php b/themes/cp_app/episode/_layout.php
index 0142abdc8006a15dd47fd6788439d16f75fc0242..16d340d2efc790589b1f1dff2948503e447a4158 100644
--- a/themes/cp_app/episode/_layout.php
+++ b/themes/cp_app/episode/_layout.php
@@ -12,10 +12,7 @@
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
 
-    <?= $this->renderSection('meta-tags') ?>
-    <?php if ($podcast->payment_pointer): ?>
-        <meta name="monetization" content="<?= $podcast->payment_pointer ?>" />
-    <?php endif; ?>
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
diff --git a/themes/cp_app/episode/_partials/preview_card.php b/themes/cp_app/episode/_partials/preview_card.php
index c0966e56869ca2231bd0cb19a0b1e86e7dbe4c22..d429d938df4cde6867d90fd1288ddd4a60c7f86c 100644
--- a/themes/cp_app/episode/_partials/preview_card.php
+++ b/themes/cp_app/episode/_partials/preview_card.php
@@ -1,4 +1,4 @@
-<div class="flex items-center border-t border-b border-subtle">
+<div class="flex items-center border-y border-subtle">
     <div class="relative">
         <time class="absolute px-1 text-sm font-semibold text-white rounded bg-black/75 bottom-2 right-2" datetime="PT<?= $episode->audio_file_duration ?>S">
                     <?= format_duration($episode->audio_file_duration) ?>
diff --git a/themes/cp_app/episode/activity.php b/themes/cp_app/episode/activity.php
index c0ca08aa779ae6fa202c42ee9abe4e763beedf54..5c9a8d7e009b84da01676c95711c8d56acc4e074 100644
--- a/themes/cp_app/episode/activity.php
+++ b/themes/cp_app/episode/activity.php
@@ -1,36 +1,5 @@
 <?= $this->extend('episode/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-<title><?= $episode->title ?></title>
-<meta name="description" content="<?= htmlspecialchars($episode->description) ?>" />
-<link rel="canonical" href="<?= $episode->link ?>" />
-<meta property="og:title" content="<?= $episode->title ?>" />
-<meta property="og:description" content="<?= $episode->description ?>" />
-<meta property="og:locale" content="<?= $podcast->language_code ?>" />
-<meta property="og:site_name" content="<?= $podcast->title ?>" />
-<meta property="og:url" content="<?= current_url() ?>" />
-<meta property="og:image" content="<?= $episode->cover->large_url ?>" />
-<meta property="og:image:width" content="<?= config('Images')
-    ->podcastCoverSizes['large'][0] ?>" />
-<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
-<meta property="og:description" content="$description" />
-<meta property="article:published_time" content="<?= $episode->published_at ?>" />
-<meta property="article:modified_time" content="<?= $episode->updated_at ?>" />
-<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" />
-<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" />
-<link rel="alternate" type="application/json+oembed" href="<?= base_url(route_to('episode-oembed-json', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed json" />
-<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" />
-<meta name="twitter:title" content="<?= $episode->title ?>" />
-<meta name="twitter:description" content="<?= $episode->description ?>" />
-<meta name="twitter:image" content="<?= $episode->cover->large_url ?>" />
-<meta name="twitter:card" content="player" />
-<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" />
-<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" />
-<meta name="twitter:player" content="<?= $episode->getEmbedUrl('light') ?>" />
-<meta name="twitter:player:width" content="600" />
-<meta name="twitter:player:height" content="200" />
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 
 <?php if (can_user_interact()): ?>
diff --git a/themes/cp_app/episode/comment.php b/themes/cp_app/episode/comment.php
index 37c1e2364cdbe577a121f78d42bb976e6115ce85..446bfb143fd2f308dd7bdcbf000a4ccc6b9dc96f 100644
--- a/themes/cp_app/episode/comment.php
+++ b/themes/cp_app/episode/comment.php
@@ -1,30 +1,13 @@
 <?= $this->extend('episode/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-    <title><?= lang('Comment.title', [
-        'actorDisplayName' => $comment->actor->display_name,
-    ]) ?></title>
-    <meta name="description" content="<?= $comment->message ?>"/>
-    <meta property="og:title" content="<?= lang('Comment.title', [
-        'actorDisplayName' => $comment->actor->display_name,
-    ]) ?>"/>
-    <meta property="og:locale" content="<?= service(
-        'request',
-    )->getLocale() ?>" />
-    <meta property="og:site_name" content="<?= $comment->actor->display_name ?>" />
-    <meta property="og:url" content="<?= current_url() ?>" />
-    <meta property="og:image" content="<?= $comment->actor->avatar_image_url ?>" />
-    <meta property="og:description" content="<?= $comment->message ?>" />
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 <div class="max-w-2xl px-6 mx-auto">
     <nav class="mb-2">
         <a href="<?= route_to('episode', $podcast->handle, $episode->slug) ?>"
         class="inline-flex items-center px-4 py-2 text-sm focus:ring-accent"><?= icon(
-        'arrow-left',
-        'mr-2 text-lg',
-    ) . lang('Comment.back_to_comments') ?></a>
+    'arrow-left',
+    'mr-2 text-lg',
+) . lang('Comment.back_to_comments') ?></a>
     </nav>
     <div class="pb-12">
         <?= $this->include('episode/_partials/comment_with_replies') ?>
diff --git a/themes/cp_app/episode/comments.php b/themes/cp_app/episode/comments.php
index 873e166874913915f05a5224ad6dbdb19af4272a..be862832c79561adeb6d4f28403d8b9e3b60692d 100644
--- a/themes/cp_app/episode/comments.php
+++ b/themes/cp_app/episode/comments.php
@@ -1,38 +1,5 @@
 <?= $this->extend('episode/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-<title><?= $episode->title ?></title>
-<meta name="description" content="<?= htmlspecialchars(
-    $episode->description,
-) ?>" />
-<link rel="canonical" href="<?= $episode->link ?>" />
-<meta property="og:title" content="<?= $episode->title ?>" />
-<meta property="og:description" content="<?= $episode->description ?>" />
-<meta property="og:locale" content="<?= $podcast->language_code ?>" />
-<meta property="og:site_name" content="<?= $podcast->title ?>" />
-<meta property="og:url" content="<?= current_url() ?>" />
-<meta property="og:image" content="<?= $episode->cover->large_url ?>" />
-<meta property="og:image:width" content="<?= config('Images')
-    ->podcastCoverSizes['large'][0] ?>" />
-<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
-<meta property="og:description" content="$description" />
-<meta property="article:published_time" content="<?= $episode->published_at ?>" />
-<meta property="article:modified_time" content="<?= $episode->updated_at ?>" />
-<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" />
-<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" />
-<link rel="alternate" type="application/json+oembed" href="<?= base_url(route_to('episode-oembed-json', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed json" />
-<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" />
-<meta name="twitter:title" content="<?= $episode->title ?>" />
-<meta name="twitter:description" content="<?= $episode->description ?>" />
-<meta name="twitter:image" content="<?= $episode->cover->large_url ?>" />
-<meta name="twitter:card" content="player" />
-<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" />
-<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" />
-<meta name="twitter:player" content="<?= $episode->getEmbedUrl('light') ?>" />
-<meta name="twitter:player:width" content="600" />
-<meta name="twitter:player:height" content="200" />
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 
 <?php if (can_user_interact()): ?>
diff --git a/themes/cp_app/home.php b/themes/cp_app/home.php
index f15b2b9e60b711717597be366b7a7fd8ac390574..708cfe331ad3476b2ef42001b14c8d07a75188dd 100644
--- a/themes/cp_app/home.php
+++ b/themes/cp_app/home.php
@@ -15,12 +15,7 @@
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
 
-    <meta property="og:title" content="<?= service('settings')
-    ->get('App.siteName') ?>" />
-    <meta property="og:description" content="<?= service('settings')
-    ->get('App.siteDescription') ?>" />
-    <meta property="og:site_name" content="<?= service('settings')
-    ->get('App.siteName') ?>" />
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
diff --git a/themes/cp_app/page.php b/themes/cp_app/page.php
deleted file mode 100644
index 148f3152fc996702f7984714d82f34d3b7ba3e5c..0000000000000000000000000000000000000000
--- a/themes/cp_app/page.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?= helper('page') ?>
-<!DOCTYPE html>
-<html lang="<?= service('request')
-    ->getLocale() ?>">
-
-<head>
-    <meta charset="UTF-8"/>
-    <title><?= $page->title ?></title>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
-    <link rel="icon" type="image/x-icon" href="<?= service('settings')
-    ->get('App.siteIcon')['ico'] ?>" />
-    <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
-    <link rel="manifest" href="<?= route_to('webmanifest') ?>">
-    <?= service('vite')
-        ->asset('styles/index.css', 'css') ?>
-    <?= service('vite')
-        ->asset('js/app.ts', 'js') ?>
-</head>
-
-<body class="flex flex-col min-h-screen mx-auto bg-base theme-<?= service('settings')
-        ->get('App.theme') ?>">
-    <?php if (service('authentication')->check()): ?>
-        <?= $this->include('_admin_navbar') ?>
-    <?php endif; ?>
-
-    <header class="py-8 border-b bg-elevated border-subtle">
-        <div class="container flex flex-col items-start px-2 py-4 mx-auto">
-            <a href="<?= route_to('home') ?>"
-            class="inline-flex items-center mb-2 focus:ring-accent"><?= icon(
-            'arrow-left',
-            'mr-2',
-        ) . lang('Page.back_to_home') ?></a>
-            <Heading tagName="h1" class="text-3xl font-semibold"><?= $page->title ?></Heading>
-        </div>
-    </header>
-    <main class="container flex-1 px-4 py-10 mx-auto">
-        <div class="prose prose-brand">
-            <?= $page->content_html ?>
-        </div>
-    </main>
-    <footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t border-subtle">
-        <?= render_page_links() ?>
-        <small><?= lang('Common.powered_by', [
-            'castopod' =>
-                '<a class="inline-flex font-semibold hover:underline focus:ring-accent" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>',
-        ]) ?></small>
-    </footer>
-</body>
diff --git a/themes/cp_app/_layout.php b/themes/cp_app/pages/_layout.php
similarity index 76%
rename from themes/cp_app/_layout.php
rename to themes/cp_app/pages/_layout.php
index d80ecb993a914b3fdd44788da5bcb2096b371891..30f9177c76e4ed45b9e21d6594ed1799574d965f 100644
--- a/themes/cp_app/_layout.php
+++ b/themes/cp_app/pages/_layout.php
@@ -5,21 +5,13 @@
 
 <head>
     <meta charset="UTF-8"/>
-    <title><?= $this->renderSection('title') ?></title>
-    <meta name="description" content="<?= service('settings')
-    ->get('App.siteDescription') ?>"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <link rel="icon" type="image/x-icon" href="<?= service('settings')
     ->get('App.siteIcon')['ico'] ?>" />
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
 
-    <meta property="og:title" content="<?= service('settings')
-    ->get('App.siteName') ?>" />
-    <meta property="og:description" content="<?= service('settings')
-    ->get('App.siteDescription') ?>" />
-    <meta property="og:site_name" content="<?= service('settings')
-    ->get('App.siteName') ?>" />
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
@@ -42,9 +34,7 @@
             'arrow-left',
             'mr-2',
         ) . lang('Page.back_to_home') ?></a>
-            <Heading tagName="h1" size="large"><?= isset($page)
-    ? $page->title
-    : 'Castopod' ?></Heading>
+            <Heading tagName="h1" size="large"><?= $page->title ?></Heading>
         </div>
     </header>
     <main class="container flex-1 px-4 py-6 mx-auto">
diff --git a/themes/cp_app/credits.php b/themes/cp_app/pages/credits.php
similarity index 98%
rename from themes/cp_app/credits.php
rename to themes/cp_app/pages/credits.php
index 44127527bf4fcc90debe9bda70430ad369c14bfd..a050bc1265ddd7ac341c84e10c75e0f842986118 100644
--- a/themes/cp_app/credits.php
+++ b/themes/cp_app/pages/credits.php
@@ -1,4 +1,4 @@
-<?= $this->extend('_layout') ?>
+<?= $this->extend('pages/_layout') ?>
 
 <?= $this->section('title') ?>
 <?= lang('Person.credits') ?>
diff --git a/themes/cp_app/map.php b/themes/cp_app/pages/map.php
similarity index 85%
rename from themes/cp_app/map.php
rename to themes/cp_app/pages/map.php
index f064a54fdfc2c170a363900fef642dbfc95cb185..7f7b966103bdb3e63d4191f0f82be3b89a96697f 100644
--- a/themes/cp_app/map.php
+++ b/themes/cp_app/pages/map.php
@@ -5,13 +5,17 @@
 
 <head>
     <meta charset="UTF-8"/>
-    <title><?= lang('Page.map') ?></title>
-    <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
+    <title><?= lang('Page.map.title') . service('settings')->get('App.siteTitleSeparator') . service('settings')->get('App.siteName') ?></title>
+    <meta name="description" content="<?= lang('Page.map.description', [
+        'siteName' => service('settings')
+            ->get('App.siteName'),
+    ]) ?>"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <link rel="icon" type="image/x-icon" href="<?= service('settings')
     ->get('App.siteIcon')['ico'] ?>" />
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
+
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
     <?= service('vite')
@@ -33,7 +37,7 @@
             'arrow-left',
             'mr-2',
         ) . lang('Page.back_to_home') ?></a>
-            <Heading tagName="h1" size="large"><?= lang('Page.map') ?></Heading>
+            <Heading tagName="h1" size="large"><?= lang('Page.map.title') ?></Heading>
         </div>
     </header>
     <main class="flex-1 w-full h-full">
diff --git a/themes/cp_app/pages/page.php b/themes/cp_app/pages/page.php
new file mode 100644
index 0000000000000000000000000000000000000000..430d75941eee2166c2dbbec9953abec9f8393829
--- /dev/null
+++ b/themes/cp_app/pages/page.php
@@ -0,0 +1,11 @@
+<?= $this->extend('pages/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.credits') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+    <div class="prose prose-brand">
+        <?= $page->content_html ?>
+    </div>
+<?= $this->endSection() ?>
\ No newline at end of file
diff --git a/themes/cp_app/podcast/_layout.php b/themes/cp_app/podcast/_layout.php
index cf38f6d0efaff9bd1a12980bf0b1bd9ab51bbb90..4c4c812bdc62566bc67934509e80b3481b505eb2 100644
--- a/themes/cp_app/podcast/_layout.php
+++ b/themes/cp_app/podcast/_layout.php
@@ -12,10 +12,7 @@
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
 
-    <?= $this->renderSection('meta-tags') ?>
-    <?php if ($podcast->payment_pointer): ?>
-        <meta name="monetization" content="<?= $podcast->payment_pointer ?>" />
-    <?php endif; ?>
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
diff --git a/themes/cp_app/podcast/about.php b/themes/cp_app/podcast/about.php
index ce00cbb1177604d3f486528798798d3e2bf9acc9..0beb5e1e480b4b668065e34326e4b6b7fe205595 100644
--- a/themes/cp_app/podcast/about.php
+++ b/themes/cp_app/podcast/about.php
@@ -1,33 +1,5 @@
 <?= $this->extend('podcast/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-<!-- TODO: -->
-
-<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>" />
-
-<title><?= $podcast->title ?></title>
-<meta name="description" content="<?= htmlspecialchars(
-    $podcast->description,
-) ?>" />
-<link rel="icon" type="image/x-icon" href="<?= service('settings')
-    ->get('App.siteIcon')['ico'] ?>" />
-<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
-<link rel="manifest" href="<?= route_to('webmanifest') ?>">
-<link rel="canonical" href="<?= current_url() ?>" />
-<meta property="og:title" content="<?= $podcast->title ?>" />
-<meta property="og:description" content="<?= $podcast->description ?>" />
-<meta property="og:locale" content="<?= $podcast->language_code ?>" />
-<meta property="og:site_name" content="<?= $podcast->title ?>" />
-<meta property="og:url" content="<?= current_url() ?>" />
-<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
-<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
-<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
-<meta name="twitter:card" content="summary_large_image" />
-
-<?= service('vite')
-    ->asset('styles/index.css', 'css') ?>
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 
 <div class="px-2 sm:px-4">
@@ -35,14 +7,14 @@
     <div class="flex gap-x-4 gap-y-2">
         <span class="px-2 py-1 text-sm font-semibold border rounded-sm border-subtle bg-highlight">
             <?= lang(
-        'Podcast.category_options.' . $podcast->category->code,
-    ) ?>
+    'Podcast.category_options.' . $podcast->category->code,
+) ?>
         </span>
         <?php foreach ($podcast->other_categories as $other_category): ?>
             <span class="px-2 py-1 text-sm font-semibold border rounded-sm border-subtle bg-highlight">
                 <?= lang(
-        'Podcast.category_options.' . $other_category->code,
-    ) ?>
+    'Podcast.category_options.' . $other_category->code,
+) ?>
             </span>
         <?php endforeach; ?>
     </div>
@@ -55,8 +27,8 @@
                     <?php foreach ($podcast->persons as $person): ?>
                         <img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-background-base last:ml-0" />
                         <?php $i++; if ($i === 3) {
-        break;
-    }?>
+    break;
+}?>
                     <?php endforeach; ?>
                 </div>
                 <?= lang('Podcast.persons', [
diff --git a/themes/cp_app/podcast/activity.php b/themes/cp_app/podcast/activity.php
index a30afc4dfdd087ac76e791374fdde3ccde54ebdd..4ca98903656919f1130142caabd7ffd05c7615ca 100644
--- a/themes/cp_app/podcast/activity.php
+++ b/themes/cp_app/podcast/activity.php
@@ -1,31 +1,5 @@
 <?= $this->extend('podcast/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>"/>
-
-<title><?= $podcast->title ?></title>
-<meta name="description" content="<?= htmlspecialchars(
-    $podcast->description,
-) ?>" />
-<link rel="icon" type="image/x-icon" href="<?= service('settings')
-    ->get('App.siteIcon')['ico'] ?>" />
-<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
-<link rel="manifest" href="<?= route_to('webmanifest') ?>">
-<link rel="canonical" href="<?= current_url() ?>" />
-<meta property="og:title" content="<?= $podcast->title ?>" />
-<meta property="og:description" content="<?= $podcast->description ?>" />
-<meta property="og:locale" content="<?= $podcast->language_code ?>" />
-<meta property="og:site_name" content="<?= $podcast->title ?>" />
-<meta property="og:url" content="<?= current_url() ?>" />
-<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
-<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
-<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
-<meta name="twitter:card" content="summary_large_image" />
-
-<?= service('vite')
-    ->asset('styles/index.css', 'css') ?>
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 
 <?php if (can_user_interact()): ?>
@@ -53,6 +27,7 @@
 <hr class="my-4 border-subtle">
 
 <?php endif; ?>
+
 <div class="flex flex-col gap-y-4">
     <?php foreach ($posts as $key => $post): ?>
         <?php if ($post->reblog_of_id !== null): ?>
diff --git a/themes/cp_app/podcast/episodes.php b/themes/cp_app/podcast/episodes.php
index 7e5b01f428b15ab925dda34ed430002ab6ea2b09..1db4ec29a86c36fe7dece9f9fba7b0eb33555d50 100644
--- a/themes/cp_app/podcast/episodes.php
+++ b/themes/cp_app/podcast/episodes.php
@@ -1,31 +1,5 @@
 <?= $this->extend('podcast/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>" />
-
-<title><?= $podcast->title ?></title>
-<meta name="description" content="<?= htmlspecialchars(
-    $podcast->description,
-) ?>" />
-<link rel="icon" type="image/x-icon" href="<?= service('settings')
-    ->get('App.siteIcon')['ico'] ?>" />
-<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
-<link rel="manifest" href="<?= route_to('webmanifest') ?>">
-<link rel="canonical" href="<?= current_url() ?>" />
-<meta property="og:title" content="<?= $podcast->title ?>" />
-<meta property="og:description" content="<?= $podcast->description ?>" />
-<meta property="og:locale" content="<?= $podcast->language_code ?>" />
-<meta property="og:site_name" content="<?= $podcast->title ?>" />
-<meta property="og:url" content="<?= current_url() ?>" />
-<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
-<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
-<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
-<meta name="twitter:card" content="summary_large_image" />
-
-<?= service('vite')
-    ->asset('styles/index.css', 'css') ?>
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 
 <?php if ($episodes): ?>
diff --git a/themes/cp_app/podcast/follow.php b/themes/cp_app/podcast/follow.php
index c491d46e098d99e516595ee7753258d0afa94d8c..0425544dee5a923d7f0789cdade76b82ee70eff6 100644
--- a/themes/cp_app/podcast/follow.php
+++ b/themes/cp_app/podcast/follow.php
@@ -12,19 +12,7 @@
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
     
-    <title><?= lang('Podcast.followTitle', [
-        'actorDisplayName' => $actor->display_name,
-    ]) ?></title>
-    <meta name="description" content="<?= $actor->summary ?>"/>
-    <meta property="og:title" content="<?= lang('Podcast.followTitle', [
-        'actorDisplayName' => $actor->display_name,
-    ]) ?>"/>
-    <meta property="og:locale" content="<?= service(
-        'request',
-    )->getLocale() ?>" />
-    <meta property="og:url" content="<?= current_url() ?>" />
-    <meta property="og:image" content="<?= $actor->avatar_image_url ?>" />
-    <meta property="og:description" content="<?= $actor->summary ?>" />
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
@@ -32,7 +20,6 @@
         ->asset('js/podcast.ts', 'js') ?>
 </head>
 
-
 <body class="flex flex-col min-h-screen bg-base theme-<?= service('settings')
         ->get('App.theme') ?>">
     <header class="flex flex-col items-center mb-8">
diff --git a/themes/cp_app/post/post.php b/themes/cp_app/post/post.php
index 1605e3fbad870e717e869072f08cc2816738f94a..02c168bf73dd382a01db3121471a1810362b0540 100644
--- a/themes/cp_app/post/post.php
+++ b/themes/cp_app/post/post.php
@@ -1,29 +1,12 @@
 <?= $this->extend('podcast/_layout') ?>
 
-<?= $this->section('meta-tags') ?>
-    <title><?= lang('Post.title', [
-        'actorDisplayName' => $post->actor->display_name,
-    ]) ?></title>
-    <meta name="description" content="<?= $post->message ?>"/>
-    <meta property="og:title" content="<?= lang('Post.title', [
-        'actorDisplayName' => $post->actor->display_name,
-    ]) ?>"/>
-    <meta property="og:locale" content="<?= service(
-        'request',
-    )->getLocale() ?>" />
-    <meta property="og:site_name" content="<?= $post->actor->display_name ?>" />
-    <meta property="og:url" content="<?= current_url() ?>" />
-    <meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
-    <meta property="og:description" content="<?= $post->message ?>" />
-<?= $this->endSection() ?>
-
 <?= $this->section('content') ?>
 <nav class="py-2">
     <a href="<?= route_to('podcast-activity', $podcast->handle) ?>"
     class="inline-flex items-center px-4 py-2 text-sm focus:ring-accent"><?= icon(
-        'arrow-left',
-        'mr-2 text-lg',
-    ) .
+    'arrow-left',
+    'mr-2 text-lg',
+) .
         lang('Post.back_to_actor_posts', [
             'actor' => $post->actor->display_name,
         ]) ?></a>
diff --git a/themes/cp_app/post/remote_action.php b/themes/cp_app/post/remote_action.php
index 2dd36fe500006e1b0c05dc44e834a8e08b3e36d1..c0d13fd46802bbf7591c36063e6ecac7f9d1739c 100644
--- a/themes/cp_app/post/remote_action.php
+++ b/themes/cp_app/post/remote_action.php
@@ -10,23 +10,7 @@
     <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
     <link rel="manifest" href="<?= route_to('webmanifest') ?>">
 
-    <title><?= lang('Fediverse.' . $action . '.title', [
-        'actorDisplayName' => $post->actor->display_name,
-    ]) ?></title>
-    <meta name="description" content="<?= $post->message ?>"/>
-    <meta property="og:title" content="<?= lang(
-        'Fediverse.' . $action . '.title',
-        [
-            'actorDisplayName' => $post->actor->display_name,
-        ],
-    ) ?>"/>
-    <meta property="og:locale" content="<?= service(
-        'request',
-    )->getLocale() ?>" />
-    <meta property="og:site_name" content="<?= $post->actor->display_name ?>" />
-    <meta property="og:url" content="<?= current_url() ?>" />
-    <meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
-    <meta property="og:description" content="<?= $post->message ?>" />
+    <?= $metatags ?>
 
     <?= service('vite')
         ->asset('styles/index.css', 'css') ?>
diff --git a/themes/cp_auth/_layout.php b/themes/cp_auth/_layout.php
index 1ac2151e8101bd888d2d0ae0e7d721ef15d09c10..fe6c3bfafefb33ddfa72e6ca2105ae3f8f9162da 100644
--- a/themes/cp_auth/_layout.php
+++ b/themes/cp_auth/_layout.php
@@ -4,6 +4,8 @@
 
 <head>
 	<meta charset="UTF-8"/>
+	<meta name="robots" content="noindex">
+
 	<title>Castopod Auth</title>
 	<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
diff --git a/themes/cp_install/_layout.php b/themes/cp_install/_layout.php
index 0de02688cba342f70280b2bd030d1638537692cd..0f9ee0d9415e0c960eda65551e6213414dfa1b04 100644
--- a/themes/cp_install/_layout.php
+++ b/themes/cp_install/_layout.php
@@ -3,7 +3,9 @@
 
 <head>
     <meta charset="UTF-8"/>
-    <title>Castopod</title>
+    <meta name="robots" content="noindex">
+
+    <title>Castopod Install</title>
     <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <link rel="icon" type="image/x-icon" href="<?= service('settings')