diff --git a/.gitignore b/.gitignore
index 28030f56d76742767dd7770054970fe5918ee080..1baa8fe3b49280b199e1fe04075ecf16cc598672 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,9 @@ $RECYCLE.BIN/
 # Linux
 *~
 
+# vim
+*.swp
+
 # KDE directory preferences
 .directory
 
@@ -135,6 +138,7 @@ node_modules
 # public folder
 public/*
 !public/media
+!public/media/~person
 !public/.htaccess
 !public/favicon.ico
 !public/index.php
@@ -144,6 +148,14 @@ public/*
 public/media/*
 !public/media/index.html
 
+# public person folder
+public/media/~person/*
+!public/media/~person/index.html
+
+# Generated files
+app/Language/en/PersonsTaxonomy.php
+app/Language/fr/PersonsTaxonomy.php
+
 #-------------------------
 # Docker volumes
 #-------------------------
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index cfbc02c9b88be296bc3cabd726a0300e7ced386a..91de34a65356f0bcf316a310bca257028622a55d 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -85,6 +85,37 @@ $routes->group(
             'as' => 'my-podcasts',
         ]);
 
+        $routes->group('persons', function ($routes) {
+            $routes->get('/', 'Person', [
+                'as' => 'person-list',
+                'filter' => 'permission:person-list',
+            ]);
+            $routes->get('new', 'Person::create', [
+                'as' => 'person-create',
+                'filter' => 'permission:person-create',
+            ]);
+            $routes->post('new', 'Person::attemptCreate', [
+                'filter' => 'permission:person-create',
+            ]);
+            $routes->group('(:num)', function ($routes) {
+                $routes->get('/', 'Person::view/$1', [
+                    'as' => 'person-view',
+                    'filter' => 'permission:person-view',
+                ]);
+                $routes->get('edit', 'Person::edit/$1', [
+                    'as' => 'person-edit',
+                    'filter' => 'permission:person-edit',
+                ]);
+                $routes->post('edit', 'Person::attemptEdit/$1', [
+                    'filter' => 'permission:person-edit',
+                ]);
+                $routes->add('delete', 'Person::delete/$1', [
+                    'as' => 'person-delete',
+                    'filter' => 'permission:person-delete',
+                ]);
+            });
+        });
+
         // Podcasts
         $routes->group('podcasts', function ($routes) {
             $routes->get('/', 'Podcast::list', [
@@ -124,6 +155,25 @@ $routes->group(
                     'filter' => 'permission:podcasts-delete',
                 ]);
 
+                $routes->group('persons', function ($routes) {
+                    $routes->get('/', 'PodcastPerson/$1', [
+                        'as' => 'podcast-person-manage',
+                        'filter' => 'permission:podcast-edit',
+                    ]);
+                    $routes->post('/', 'PodcastPerson::attemptAdd/$1', [
+                        'filter' => 'permission:podcast-edit',
+                    ]);
+
+                    $routes->get(
+                        '(:num)/remove',
+                        'PodcastPerson::remove/$1/$2',
+                        [
+                            'as' => 'podcast-person-remove',
+                            'filter' => 'permission:podcast-edit',
+                        ]
+                    );
+                });
+
                 $routes->group('analytics', function ($routes) {
                     $routes->get('/', 'Podcast::viewAnalytics/$1', [
                         'as' => 'podcast-analytics',
@@ -276,6 +326,30 @@ $routes->group(
                                 'filter' => 'permission:podcast_episodes-edit',
                             ]
                         );
+
+                        $routes->group('persons', function ($routes) {
+                            $routes->get('/', 'EpisodePerson/$1/$2', [
+                                'as' => 'episode-person-manage',
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ]);
+                            $routes->post(
+                                '/',
+                                'EpisodePerson::attemptAdd/$1/$2',
+                                [
+                                    'filter' =>
+                                        'permission:podcast_episodes-edit',
+                                ]
+                            );
+                            $routes->get(
+                                '(:num)/remove',
+                                'EpisodePerson::remove/$1/$2/$3',
+                                [
+                                    'as' => 'episode-person-remove',
+                                    'filter' =>
+                                        'permission:podcast_episodes-edit',
+                                ]
+                            );
+                        });
                     });
                 });
 
@@ -497,6 +571,7 @@ $routes->group('@(:podcastName)', function ($routes) {
     $routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
     $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
 });
+$routes->get('/credits', 'Page::credits', ['as' => 'credits']);
 $routes->get('/(:slug)', 'Page/$1', ['as' => 'page']);
 
 /**
diff --git a/app/Controllers/Admin/EpisodePerson.php b/app/Controllers/Admin/EpisodePerson.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d35dd9c2b9d99fa6c66d89fa08a0c662be15588
--- /dev/null
+++ b/app/Controllers/Admin/EpisodePerson.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers\Admin;
+
+use App\Models\EpisodePersonModel;
+use App\Models\PodcastModel;
+use App\Models\EpisodeModel;
+use App\Models\PersonModel;
+
+class EpisodePerson extends BaseController
+{
+    /**
+     * @var \App\Entities\Podcast
+     */
+    protected $podcast;
+
+    /**
+     * @var \App\Entities\Episode
+     */
+    protected $episode;
+
+    public function _remap($method, ...$params)
+    {
+        if (count($params) > 1) {
+            if (
+                !($this->podcast = (new PodcastModel())->getPodcastById(
+                    $params[0]
+                ))
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+            if (
+                !($this->episode = (new EpisodeModel())
+                    ->where([
+                        'id' => $params[1],
+                        'podcast_id' => $params[0],
+                    ])
+                    ->first())
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+        } else {
+            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+        }
+        unset($params[1]);
+        unset($params[0]);
+
+        return $this->$method(...$params);
+    }
+
+    public function index()
+    {
+        helper('form');
+
+        $data = [
+            'episode' => $this->episode,
+            'podcast' => $this->podcast,
+            'episodePersons' => (new EpisodePersonModel())->getPersonsByEpisodeId(
+                $this->podcast->id,
+                $this->episode->id
+            ),
+            'personOptions' => (new PersonModel())->getPersonOptions(),
+            'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
+        ];
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+            1 => $this->episode->title,
+        ]);
+        return view('admin/episode/person', $data);
+    }
+
+    public function attemptAdd()
+    {
+        $rules = [
+            'person' => 'required',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        (new EpisodePersonModel())->addEpisodePersons(
+            $this->podcast->id,
+            $this->episode->id,
+            $this->request->getPost('person'),
+            $this->request->getPost('person_group_role')
+        );
+
+        return redirect()->back();
+    }
+
+    public function remove($episodePersonId)
+    {
+        (new EpisodePersonModel())->removeEpisodePersons(
+            $this->podcast->id,
+            $this->episode->id,
+            $episodePersonId
+        );
+
+        return redirect()->back();
+    }
+}
diff --git a/app/Controllers/Admin/Person.php b/app/Controllers/Admin/Person.php
new file mode 100644
index 0000000000000000000000000000000000000000..f78631ff5e11bd84601aca0d8ee8747ff744e9e3
--- /dev/null
+++ b/app/Controllers/Admin/Person.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers\Admin;
+
+use App\Models\PersonModel;
+
+class Person extends BaseController
+{
+    /**
+     * @var \App\Entities\Person|null
+     */
+    protected $person;
+
+    public function _remap($method, ...$params)
+    {
+        if (count($params) > 0) {
+            if (
+                !($this->person = (new PersonModel())->getPersonById(
+                    $params[0]
+                ))
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+        }
+
+        return $this->$method();
+    }
+
+    public function index()
+    {
+        $data = ['persons' => (new PersonModel())->findAll()];
+
+        return view('admin/person/list', $data);
+    }
+
+    public function view()
+    {
+        $data = ['person' => $this->person];
+
+        replace_breadcrumb_params([0 => $this->person->full_name]);
+        return view('admin/person/view', $data);
+    }
+
+    public function create()
+    {
+        helper(['form']);
+
+        return view('admin/person/create');
+    }
+
+    public function attemptCreate()
+    {
+        $rules = [
+            'image' =>
+                'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        $person = new \App\Entities\Person([
+            'full_name' => $this->request->getPost('full_name'),
+            'unique_name' => $this->request->getPost('unique_name'),
+            'information_url' => $this->request->getPost('information_url'),
+            'image' => $this->request->getFile('image'),
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+        ]);
+
+        $personModel = new PersonModel();
+
+        if (!$personModel->insert($person)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $personModel->errors());
+        }
+
+        return redirect()->route('person-list');
+    }
+
+    public function edit()
+    {
+        helper('form');
+
+        $data = [
+            'person' => $this->person,
+        ];
+
+        replace_breadcrumb_params([0 => $this->person->full_name]);
+        return view('admin/person/edit', $data);
+    }
+
+    public function attemptEdit()
+    {
+        $rules = [
+            'image' =>
+                'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        $this->person->full_name = $this->request->getPost('full_name');
+        $this->person->unique_name = $this->request->getPost('unique_name');
+        $this->person->information_url = $this->request->getPost(
+            'information_url'
+        );
+        $image = $this->request->getFile('image');
+        if ($image->isValid()) {
+            $this->person->image = $image;
+        }
+
+        $this->updated_by = user();
+
+        $personModel = new PersonModel();
+        if (!$personModel->update($this->person->id, $this->person)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $personModel->errors());
+        }
+
+        return redirect()->route('person-view', [$this->person->id]);
+    }
+
+    public function delete()
+    {
+        (new PersonModel())->delete($this->person->id);
+
+        return redirect()->route('person-list');
+    }
+}
diff --git a/app/Controllers/Admin/PodcastImport.php b/app/Controllers/Admin/PodcastImport.php
index 0ae92f15caebbcb02bc0751b04da5a6809b5e74d..05625077a8471a5725f1a391ae9a4fe459df8d40 100644
--- a/app/Controllers/Admin/PodcastImport.php
+++ b/app/Controllers/Admin/PodcastImport.php
@@ -13,6 +13,9 @@ use App\Models\LanguageModel;
 use App\Models\PodcastModel;
 use App\Models\EpisodeModel;
 use App\Models\PlatformModel;
+use App\Models\PersonModel;
+use App\Models\PodcastPersonModel;
+use App\Models\EpisodePersonModel;
 use Config\Services;
 use League\HTMLToMarkdown\HtmlConverter;
 
@@ -150,7 +153,7 @@ class PodcastImport extends BaseController
                     : $nsItunes->complete === 'yes',
                 'location_name' => !$nsPodcast->location
                     ? null
-                    : $nsPodcast->location->attributes()['name'],
+                    : $nsPodcast->location,
                 'location_geo' =>
                     !$nsPodcast->location ||
                     empty($nsPodcast->location->attributes()['geo'])
@@ -158,9 +161,9 @@ class PodcastImport extends BaseController
                         : $nsPodcast->location->attributes()['geo'],
                 'location_osmid' =>
                     !$nsPodcast->location ||
-                    empty($nsPodcast->location->attributes()['osmid'])
+                    empty($nsPodcast->location->attributes()['osm'])
                         ? null
-                        : $nsPodcast->location->attributes()['osmid'],
+                        : $nsPodcast->location->attributes()['osm'],
                 'created_by' => user(),
                 'updated_by' => user(),
             ]);
@@ -200,40 +203,40 @@ class PodcastImport extends BaseController
             $podcastAdminGroup->id
         );
 
-        $platformModel = new PlatformModel();
         $podcastsPlatformsData = [];
-        foreach ($nsPodcast->id as $podcastingPlatform) {
-            $slug = $podcastingPlatform->attributes()['platform'];
-            $platformModel->getOrCreatePlatform($slug, 'podcasting');
-            array_push($podcastsPlatformsData, [
-                'platform_slug' => $slug,
-                'podcast_id' => $newPodcastId,
-                'link_url' => $podcastingPlatform->attributes()['url'],
-                'link_content' => $podcastingPlatform->attributes()['id'],
-                'is_visible' => false,
-            ]);
-        }
-        foreach ($nsPodcast->social as $socialPlatform) {
-            $slug = $socialPlatform->attributes()['platform'];
-            $platformModel->getOrCreatePlatform($slug, 'social');
-            array_push($podcastsPlatformsData, [
-                'platform_slug' => $socialPlatform->attributes()['platform'],
-                'podcast_id' => $newPodcastId,
-                'link_url' => $socialPlatform->attributes()['url'],
-                'link_content' => $socialPlatform,
-                'is_visible' => false,
-            ]);
-        }
-        foreach ($nsPodcast->funding as $fundingPlatform) {
-            $slug = $fundingPlatform->attributes()['platform'];
-            $platformModel->getOrCreatePlatform($slug, 'funding');
-            array_push($podcastsPlatformsData, [
-                'platform_slug' => $fundingPlatform->attributes()['platform'],
-                'podcast_id' => $newPodcastId,
-                'link_url' => $fundingPlatform->attributes()['url'],
-                'link_content' => $fundingPlatform->attributes()['id'],
-                'is_visible' => false,
-            ]);
+        $platformTypes = [
+            ['name' => 'podcasting', 'elements' => $nsPodcast->id],
+            ['name' => 'social', 'elements' => $nsPodcast->social],
+            ['name' => 'funding', 'elements' => $nsPodcast->funding],
+        ];
+        $platformModel = new PlatformModel();
+        foreach ($platformTypes as $platformType) {
+            foreach ($platformType['elements'] as $platform) {
+                $platformLabel = $platform->attributes()['platform'];
+                $platformSlug = slugify($platformLabel);
+                if (!$platformModel->getPlatform($platformSlug)) {
+                    if (
+                        !$platformModel->createPlatform(
+                            $platformSlug,
+                            $platformType['name'],
+                            $platformLabel,
+                            ''
+                        )
+                    ) {
+                        return redirect()
+                            ->back()
+                            ->withInput()
+                            ->with('errors', $platformModel->errors());
+                    }
+                }
+                array_push($podcastsPlatformsData, [
+                    'platform_slug' => $platformSlug,
+                    'podcast_id' => $newPodcastId,
+                    'link_url' => $platform->attributes()['url'],
+                    'link_content' => $platform->attributes()['id'],
+                    'is_visible' => false,
+                ]);
+            }
         }
         if (count($podcastsPlatformsData) > 1) {
             $platformModel->createPodcastPlatforms(
@@ -242,6 +245,54 @@ class PodcastImport extends BaseController
             );
         }
 
+        foreach ($nsPodcast->person as $podcastPerson) {
+            $personModel = new PersonModel();
+            $newPersonId = null;
+            if ($newPerson = $personModel->getPerson($podcastPerson)) {
+                $newPersonId = $newPerson->id;
+            } else {
+                if (
+                    !($newPersonId = $personModel->createPerson(
+                        $podcastPerson,
+                        $podcastPerson->attributes()['href'],
+                        $podcastPerson->attributes()['img']
+                    ))
+                ) {
+                    return redirect()
+                        ->back()
+                        ->withInput()
+                        ->with('errors', $personModel->errors());
+                }
+            }
+
+            $personGroup = empty($podcastPerson->attributes()['group'])
+                ? ['slug' => '']
+                : \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
+                    (string) $podcastPerson->attributes()['group']
+                ];
+            $personRole =
+                empty($podcastPerson->attributes()['role']) ||
+                empty($personGroup)
+                    ? ['slug' => '']
+                    : $personGroup['roles'][
+                        strval($podcastPerson->attributes()['role'])
+                    ];
+            $newPodcastPerson = new \App\Entities\PodcastPerson([
+                'podcast_id' => $newPodcastId,
+                'person_id' => $newPersonId,
+                'person_group' => $personGroup['slug'],
+                'person_role' => $personRole['slug'],
+            ]);
+            $podcastPersonModel = new PodcastPersonModel();
+
+            if (!$podcastPersonModel->insert($newPodcastPerson)) {
+                return redirect()
+                    ->back()
+                    ->withInput()
+                    ->with('errors', $podcastPersonModel->errors());
+            }
+        }
+
         $numberItems = $feed->channel[0]->item->count();
         $lastItem =
             !empty($this->request->getPost('max_episodes')) &&
@@ -251,6 +302,7 @@ class PodcastImport extends BaseController
 
         $slugs = [];
 
+        //////////////////////////////////////////////////////////////////
         // For each Episode:
         for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) {
             $item = $feed->channel[0]->item[$numberItems - $itemNumber];
@@ -326,7 +378,7 @@ class PodcastImport extends BaseController
                     : $nsItunes->block === 'yes',
                 'location_name' => !$nsPodcast->location
                     ? null
-                    : $nsPodcast->location->attributes()['name'],
+                    : $nsPodcast->location,
                 'location_geo' =>
                     !$nsPodcast->location ||
                     empty($nsPodcast->location->attributes()['geo'])
@@ -334,9 +386,9 @@ class PodcastImport extends BaseController
                         : $nsPodcast->location->attributes()['geo'],
                 'location_osmid' =>
                     !$nsPodcast->location ||
-                    empty($nsPodcast->location->attributes()['osmid'])
+                    empty($nsPodcast->location->attributes()['osm'])
                         ? null
-                        : $nsPodcast->location->attributes()['osmid'],
+                        : $nsPodcast->location->attributes()['osm'],
                 'created_by' => user(),
                 'updated_by' => user(),
                 'published_at' => strtotime($item->pubDate),
@@ -344,13 +396,62 @@ class PodcastImport extends BaseController
 
             $episodeModel = new EpisodeModel();
 
-            if (!$episodeModel->insert($newEpisode)) {
+            if (!($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
                 // FIXME: What shall we do?
                 return redirect()
                     ->back()
                     ->withInput()
                     ->with('errors', $episodeModel->errors());
             }
+
+            foreach ($nsPodcast->person as $episodePerson) {
+                $personModel = new PersonModel();
+                $newPersonId = null;
+                if ($newPerson = $personModel->getPerson($episodePerson)) {
+                    $newPersonId = $newPerson->id;
+                } else {
+                    if (
+                        !($newPersonId = $personModel->createPerson(
+                            $episodePerson,
+                            $episodePerson->attributes()['href'],
+                            $episodePerson->attributes()['img']
+                        ))
+                    ) {
+                        return redirect()
+                            ->back()
+                            ->withInput()
+                            ->with('errors', $personModel->errors());
+                    }
+                }
+
+                $personGroup = empty($episodePerson->attributes()['group'])
+                    ? ['slug' => '']
+                    : \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
+                        strval($episodePerson->attributes()['group'])
+                    ];
+                $personRole =
+                    empty($episodePerson->attributes()['role']) ||
+                    empty($personGroup)
+                        ? ['slug' => '']
+                        : $personGroup['roles'][
+                            strval($episodePerson->attributes()['role'])
+                        ];
+                $newEpisodePerson = new \App\Entities\PodcastPerson([
+                    'podcast_id' => $newPodcastId,
+                    'episode_id' => $newEpisodeId,
+                    'person_id' => $newPersonId,
+                    'person_group' => $personGroup['slug'],
+                    'person_role' => $personRole['slug'],
+                ]);
+                $episodePersonModel = new EpisodePersonModel();
+
+                if (!$episodePersonModel->insert($newEpisodePerson)) {
+                    return redirect()
+                        ->back()
+                        ->withInput()
+                        ->with('errors', $episodePersonModel->errors());
+                }
+            }
         }
 
         $db->transComplete();
diff --git a/app/Controllers/Admin/PodcastPerson.php b/app/Controllers/Admin/PodcastPerson.php
new file mode 100644
index 0000000000000000000000000000000000000000..676037007db6f5c4f4c3883818c73f4b030fb822
--- /dev/null
+++ b/app/Controllers/Admin/PodcastPerson.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers\Admin;
+
+use App\Models\PodcastPersonModel;
+use App\Models\PodcastModel;
+use App\Models\PersonModel;
+
+class PodcastPerson extends BaseController
+{
+    /**
+     * @var \App\Entities\Podcast
+     */
+    protected $podcast;
+
+    public function _remap($method, ...$params)
+    {
+        if (count($params) > 0) {
+            if (
+                !($this->podcast = (new PodcastModel())->getPodcastById(
+                    $params[0]
+                ))
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+        } else {
+            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+        }
+        unset($params[0]);
+
+        return $this->$method(...$params);
+    }
+
+    public function index()
+    {
+        helper('form');
+
+        $data = [
+            'podcast' => $this->podcast,
+            'podcastPersons' => (new PodcastPersonModel())->getPersonsByPodcastId(
+                $this->podcast->id
+            ),
+            'personOptions' => (new PersonModel())->getPersonOptions(),
+            'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
+        ];
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+        ]);
+        return view('admin/podcast/person', $data);
+    }
+
+    public function attemptAdd()
+    {
+        $rules = [
+            'person' => 'required',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        (new PodcastPersonModel())->addPodcastPersons(
+            $this->podcast->id,
+            $this->request->getPost('person'),
+            $this->request->getPost('person_group_role')
+        );
+
+        return redirect()->back();
+    }
+
+    public function remove($podcastPersonId)
+    {
+        (new PodcastPersonModel())->removePodcastPersons(
+            $this->podcast->id,
+            $podcastPersonId
+        );
+
+        return redirect()->back();
+    }
+}
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 7b5dc9f7ab10cca53c4ad91728984fdc3b10cbaf..3df89dda870acb533787256340b2b827cf255778 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -54,11 +54,55 @@ class Episode extends BaseController
                 $this->podcast->type
             );
 
+            $persons = [];
+            foreach ($this->episode->episode_persons as $episodePerson) {
+                if (array_key_exists($episodePerson->person->id, $persons)) {
+                    $persons[$episodePerson->person->id]['roles'] .=
+                        empty($episodePerson->person_group) ||
+                        empty($episodePerson->person_role)
+                            ? ''
+                            : (empty(
+                                    $persons[$episodePerson->person->id][
+                                        'roles'
+                                    ]
+                                )
+                                    ? ''
+                                    : ', ') .
+                                lang(
+                                    'PersonsTaxonomy.persons.' .
+                                        $episodePerson->person_group .
+                                        '.roles.' .
+                                        $episodePerson->person_role .
+                                        '.label'
+                                );
+                } else {
+                    $persons[$episodePerson->person->id] = [
+                        'full_name' => $episodePerson->person->full_name,
+                        'information_url' =>
+                            $episodePerson->person->information_url,
+                        'thumbnail_url' =>
+                            $episodePerson->person->image->thumbnail_url,
+                        'roles' =>
+                            empty($episodePerson->person_group) ||
+                            empty($episodePerson->person_role)
+                                ? ''
+                                : lang(
+                                    'PersonsTaxonomy.persons.' .
+                                        $episodePerson->person_group .
+                                        '.roles.' .
+                                        $episodePerson->person_role .
+                                        '.label'
+                                ),
+                    ];
+                }
+            }
+
             $data = [
                 'previousEpisode' => $previousNextEpisodes['previous'],
                 'nextEpisode' => $previousNextEpisodes['next'],
                 'podcast' => $this->podcast,
                 'episode' => $this->episode,
+                'persons' => $persons,
             ];
 
             $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
diff --git a/app/Controllers/Page.php b/app/Controllers/Page.php
index b30b5fd6e479a7210fe326dd6b4c82c5edf8a171..74735dee6ad3272bdbd809dcabb58e3c8ec423eb 100644
--- a/app/Controllers/Page.php
+++ b/app/Controllers/Page.php
@@ -9,6 +9,8 @@
 namespace App\Controllers;
 
 use App\Models\PageModel;
+use App\Models\CreditModel;
+use App\Models\PodcastModel;
 
 class Page extends BaseController
 {
@@ -42,4 +44,137 @@ class Page extends BaseController
         ];
         return view('page', $data);
     }
+
+    public function credits()
+    {
+        $locale = service('request')->getLocale();
+        $model = new PodcastModel();
+        $allPodcasts = $model->findAll();
+
+        if (!($found = cache("credits_{$locale}"))) {
+            $page = new \App\Entities\Page([
+                'title' => lang('Person.credits', [], $locale),
+                'slug' => 'credits',
+                'content' => '',
+            ]);
+
+            $creditModel = (new CreditModel())->findAll();
+
+            // Unlike the carpenter, we make a tree from a table:
+
+            $person_group = null;
+            $person_id = null;
+            $person_role = null;
+            $credits = [];
+            foreach ($creditModel as $credit) {
+                if ($person_group !== $credit->person_group) {
+                    $person_group = $credit->person_group;
+                    $person_id = $credit->person_id;
+                    $person_role = $credit->person_role;
+                    $credits[$person_group] = [
+                        'group_label' => $credit->group_label,
+                        'persons' => [
+                            $person_id => [
+                                'full_name' => $credit->person->full_name,
+                                'thumbnail_url' =>
+                                    $credit->person->image->thumbnail_url,
+                                'information_url' =>
+                                    $credit->person->information_url,
+                                'roles' => [
+                                    $person_role => [
+                                        'role_label' => $credit->role_label,
+                                        'is_in' => [
+                                            [
+                                                'link' => $credit->episode
+                                                    ? $credit->episode->link
+                                                    : $credit->podcast->link,
+                                                'title' => $credit->episode
+                                                    ? (count($allPodcasts) > 1
+                                                            ? "{$credit->podcast->title} â–¸ "
+                                                            : '') .
+                                                        "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
+                                                    : $credit->podcast->title,
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ];
+                } elseif ($person_id !== $credit->person_id) {
+                    $person_id = $credit->person_id;
+                    $person_role = $credit->person_role;
+                    $credits[$person_group]['persons'][$person_id] = [
+                        'full_name' => $credit->person->full_name,
+                        'thumbnail_url' =>
+                            $credit->person->image->thumbnail_url,
+                        'information_url' => $credit->person->information_url,
+                        'roles' => [
+                            $person_role => [
+                                'role_label' => $credit->role_label,
+                                'is_in' => [
+                                    [
+                                        'link' => $credit->episode
+                                            ? $credit->episode->link
+                                            : $credit->podcast->link,
+                                        'title' => $credit->episode
+                                            ? (count($allPodcasts) > 1
+                                                    ? "{$credit->podcast->title} â–¸ "
+                                                    : '') .
+                                                "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
+                                            : $credit->podcast->title,
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ];
+                } elseif ($person_role !== $credit->person_role) {
+                    $person_role = $credit->person_role;
+                    $credits[$person_group]['persons'][$person_id]['roles'][
+                        $person_role
+                    ] = [
+                        'role_label' => $credit->role_label,
+                        'is_in' => [
+                            [
+                                'link' => $credit->episode
+                                    ? $credit->episode->link
+                                    : $credit->podcast->link,
+                                'title' => $credit->episode
+                                    ? (count($allPodcasts) > 1
+                                            ? "{$credit->podcast->title} â–¸ "
+                                            : '') .
+                                        "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
+                                    : $credit->podcast->title,
+                            ],
+                        ],
+                    ];
+                } else {
+                    $credits[$person_group]['persons'][$person_id]['roles'][
+                        $person_role
+                    ]['is_in'][] = [
+                        'link' => $credit->episode
+                            ? $credit->episode->link
+                            : $credit->podcast->link,
+                        'title' => $credit->episode
+                            ? (count($allPodcasts) > 1
+                                    ? "{$credit->podcast->title} â–¸ "
+                                    : '') .
+                                "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
+                            : $credit->podcast->title,
+                    ];
+                }
+            }
+
+            $data = [
+                'page' => $page,
+                'credits' => $credits,
+            ];
+
+            $found = view('credits', $data);
+
+            cache()->save("credits_{$locale}", $found, DECADE);
+        }
+
+        return $found;
+    }
 }
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 1e1dbda3b31bdeb5da027eff837cef30a993f0bb..9c6fc6756e19feca366c76f0233790a877ed03e3 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -109,6 +109,49 @@ class Podcast extends BaseController
                 ]);
             }
 
+            $persons = [];
+            foreach ($this->podcast->podcast_persons as $podcastPerson) {
+                if (array_key_exists($podcastPerson->person->id, $persons)) {
+                    $persons[$podcastPerson->person->id]['roles'] .=
+                        empty($podcastPerson->person_group) ||
+                        empty($podcastPerson->person_role)
+                            ? ''
+                            : (empty(
+                                    $persons[$podcastPerson->person->id][
+                                        'roles'
+                                    ]
+                                )
+                                    ? ''
+                                    : ', ') .
+                                lang(
+                                    'PersonsTaxonomy.persons.' .
+                                        $podcastPerson->person_group .
+                                        '.roles.' .
+                                        $podcastPerson->person_role .
+                                        '.label'
+                                );
+                } else {
+                    $persons[$podcastPerson->person->id] = [
+                        'full_name' => $podcastPerson->person->full_name,
+                        'information_url' =>
+                            $podcastPerson->person->information_url,
+                        'thumbnail_url' =>
+                            $podcastPerson->person->image->thumbnail_url,
+                        'roles' =>
+                            empty($podcastPerson->person_group) ||
+                            empty($podcastPerson->person_role)
+                                ? ''
+                                : lang(
+                                    'PersonsTaxonomy.persons.' .
+                                        $podcastPerson->person_group .
+                                        '.roles.' .
+                                        $podcastPerson->person_role .
+                                        '.label'
+                                ),
+                    ];
+                }
+            }
+
             $data = [
                 'podcast' => $this->podcast,
                 'episodesNav' => $episodesNavigation,
@@ -119,6 +162,7 @@ class Podcast extends BaseController
                     $yearQuery,
                     $seasonQuery
                 ),
+                'personArray' => $persons,
             ];
 
             $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
diff --git a/app/Database/Migrations/2020-06-05-190000_add_platforms.php b/app/Database/Migrations/2020-06-05-190000_add_platforms.php
index bbb231e9c0b5b2d89bf654bd0883330a75d66c9c..b79e7939da65ec5b81f367dd6c6aadb26937b4c5 100644
--- a/app/Database/Migrations/2020-06-05-190000_add_platforms.php
+++ b/app/Database/Migrations/2020-06-05-190000_add_platforms.php
@@ -41,11 +41,9 @@ class AddPlatforms extends Migration
                 'default' => null,
             ],
         ]);
+        $this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
         $this->forge->addField(
-            '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
-        );
-        $this->forge->addField(
-            '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+            '`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()'
         );
         $this->forge->addKey('slug', true);
         $this->forge->createTable('platforms');
diff --git a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
index c353a5d7a166c01433de7d790aed8ffdf89216f7..045add8548aeb5844618560c801d5289f76a0b71 100644
--- a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
+++ b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
@@ -40,12 +40,6 @@ class AddPodcastsPlatforms extends Migration
                 'constraint' => 1,
                 'default' => 0,
             ],
-            'created_at' => [
-                'type' => 'DATETIME',
-            ],
-            'updated_at' => [
-                'type' => 'DATETIME',
-            ],
         ]);
 
         $this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
diff --git a/app/Database/Migrations/2020-12-25-120000_add_persons.php b/app/Database/Migrations/2020-12-25-120000_add_persons.php
new file mode 100644
index 0000000000000000000000000000000000000000..bacdafcc07373399262525f0c4541c72675c8a12
--- /dev/null
+++ b/app/Database/Migrations/2020-12-25-120000_add_persons.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * Class Persons
+ * Creates persons table in database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddPersons extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+                'auto_increment' => true,
+            ],
+            'full_name' => [
+                'type' => 'VARCHAR',
+                'constraint' => 192,
+                'comment' => 'This is the full name or alias of the person.',
+            ],
+            'unique_name' => [
+                'type' => 'VARCHAR',
+                'constraint' => 192,
+                'comment' => 'This is the slug name or alias of the person.',
+                'unique' => true,
+            ],
+            'information_url' => [
+                'type' => 'VARCHAR',
+                'constraint' => 512,
+                'comment' =>
+                    'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
+                'null' => true,
+            ],
+            'image_uri' => [
+                'type' => 'VARCHAR',
+                'constraint' => 255,
+            ],
+            'created_by' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'updated_by' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'created_at' => [
+                'type' => 'DATETIME',
+            ],
+            'updated_at' => [
+                'type' => 'DATETIME',
+            ],
+        ]);
+
+        $this->forge->addKey('id', true);
+        $this->forge->addForeignKey('created_by', 'users', 'id');
+        $this->forge->addForeignKey('updated_by', 'users', 'id');
+        $this->forge->createTable('persons');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('persons');
+    }
+}
diff --git a/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php b/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php
new file mode 100644
index 0000000000000000000000000000000000000000..1e7bc16b2d44ecd3d05792924d61390fa82e7672
--- /dev/null
+++ b/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Class AddPodcastsPersons
+ * Creates podcasts_persons table in database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddPodcastsPersons extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+                'auto_increment' => true,
+            ],
+            'podcast_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'person_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'person_group' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+            'person_role' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+        ]);
+        $this->forge->addKey('id', true);
+        $this->forge->addUniqueKey([
+            'podcast_id',
+            'person_id',
+            'person_group',
+            'person_role',
+        ]);
+        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->addForeignKey('person_id', 'persons', 'id');
+        $this->forge->createTable('podcasts_persons');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('podcasts_persons');
+    }
+}
diff --git a/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php b/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php
new file mode 100644
index 0000000000000000000000000000000000000000..4c1c6383f7a8cac61aee1bcfe890a1e3754baecc
--- /dev/null
+++ b/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Class AddEpisodesPersons
+ * Creates episodes_persons table in database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddEpisodesPersons extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+                'auto_increment' => true,
+            ],
+            'podcast_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'episode_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'person_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'person_group' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+            'person_role' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+        ]);
+        $this->forge->addKey('id', true);
+        $this->forge->addUniqueKey([
+            'podcast_id',
+            'episode_id',
+            'person_id',
+            'person_group',
+            'person_role',
+        ]);
+        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->addForeignKey('episode_id', 'episodes', 'id');
+        $this->forge->addForeignKey('person_id', 'persons', 'id');
+        $this->forge->createTable('episodes_persons');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('episodes_persons');
+    }
+}
diff --git a/app/Database/Migrations/2020-12-25-150000_add_credit_view.php b/app/Database/Migrations/2020-12-25-150000_add_credit_view.php
new file mode 100644
index 0000000000000000000000000000000000000000..42731dfcba6e317025c9afe3a8e1cabe3f933e6b
--- /dev/null
+++ b/app/Database/Migrations/2020-12-25-150000_add_credit_view.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Class AddCreditView
+ * Creates Credit View in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddCreditView extends Migration
+{
+    public function up()
+    {
+        // Creates View for credit UNION query
+        $viewName = $this->db->prefixTable('credits');
+        $personTable = $this->db->prefixTable('persons');
+        $podcastPersonTable = $this->db->prefixTable('podcasts_persons');
+        $episodePersonTable = $this->db->prefixTable('episodes_persons');
+        $createQuery = <<<EOD
+CREATE VIEW `$viewName` AS
+    SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable`
+        INNER JOIN `$personTable`
+            ON (`person_id`=`$personTable`.`id`)
+    UNION
+    SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable`
+        INNER JOIN `$personTable`
+            ON (`person_id`=`$personTable`.`id`)
+    ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
+EOD;
+        $this->db->query($createQuery);
+    }
+
+    public function down()
+    {
+        $viewName = $this->db->prefixTable('credits');
+        $this->db->query("DROP VIEW IF EXISTS `$viewName`");
+    }
+}
diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php
index 8c509669f5968f1b1ee7e57c2c11cf544af714c3..eb567ad0911f6bf0c8b2cb03f60108b876c068f2 100644
--- a/app/Database/Seeds/AuthSeeder.php
+++ b/app/Database/Seeds/AuthSeeder.php
@@ -198,6 +198,33 @@ class AuthSeeder extends Seeder
                 'has_permission' => ['podcast_admin'],
             ],
         ],
+        'person' => [
+            [
+                'name' => 'create',
+                'description' => 'Add a new person',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'list',
+                'description' => 'List all persons',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'view',
+                'description' => 'View any person',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'edit',
+                'description' => 'Edit a person',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' => 'Delete any person from the database',
+                'has_permission' => ['superadmin'],
+            ],
+        ],
     ];
 
     static function getGroupIdByName($name, $dataGroups)
diff --git a/app/Database/Seeds/PlatformSeeder.php b/app/Database/Seeds/PlatformSeeder.php
index c6df88a0165acc93c6e1c33d1ae62faab2259486..4e0bdb631566edf22608ca474f78d184e03524d5 100644
--- a/app/Database/Seeds/PlatformSeeder.php
+++ b/app/Database/Seeds/PlatformSeeder.php
@@ -47,6 +47,13 @@ class PlatformSeeder extends Seeder
                 'home_url' => 'https://www.blubrry.com/',
                 'submit_url' => 'https://www.blubrry.com/addpodcast.php',
             ],
+            [
+                'slug' => 'breaker',
+                'type' => 'podcasting',
+                'label' => 'Breaker',
+                'home_url' => 'https://www.breaker.audio/',
+                'submit_url' => 'https://podcasters.breaker.audio/',
+            ],
             [
                 'slug' => 'castbox',
                 'type' => 'podcasting',
diff --git a/app/Entities/Credit.php b/app/Entities/Credit.php
new file mode 100644
index 0000000000000000000000000000000000000000..0988e7ca15fc8d621d2a94aecf7632a24a7cb64b
--- /dev/null
+++ b/app/Entities/Credit.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use App\Models\PersonModel;
+use App\Models\PodcastModel;
+use App\Models\EpisodeModel;
+
+use CodeIgniter\Entity;
+
+class Credit extends Entity
+{
+    /**
+     * @var \App\Entities\Person
+     */
+    protected $person;
+
+    /**
+     * @var \App\Entities\Podcast
+     */
+    protected $podcast;
+
+    /**
+     * @var \App\Entities\Episode
+     */
+    protected $episode;
+
+    /**
+     * @var string
+     */
+    protected $group_label;
+
+    /**
+     * @var string
+     */
+    protected $role_label;
+
+    public function getPodcast()
+    {
+        return (new PodcastModel())->getPodcastById(
+            $this->attributes['podcast_id']
+        );
+    }
+
+    public function getEpisode()
+    {
+        if (empty($this->attributes['episode_id'])) {
+            return null;
+        } else {
+            return (new EpisodeModel())->getEpisodeById(
+                $this->attributes['podcast_id'],
+                $this->attributes['episode_id']
+            );
+        }
+    }
+
+    public function getPerson()
+    {
+        return (new PersonModel())->getPersonById(
+            $this->attributes['person_id']
+        );
+    }
+
+    public function getGroupLabel()
+    {
+        if (empty($this->attributes['person_group'])) {
+            return null;
+        } else {
+            return lang(
+                "PersonsTaxonomy.persons.{$this->attributes['person_group']}.label"
+            );
+        }
+    }
+
+    public function getRoleLabel()
+    {
+        if (
+            empty($this->attributes['person_group']) ||
+            empty($this->attributes['person_role'])
+        ) {
+            return null;
+        } else {
+            return lang(
+                "PersonsTaxonomy.persons.{$this->attributes['person_group']}.roles.{$this->attributes['person_role']}.label"
+            );
+        }
+    }
+}
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 4249defe8775a476dc863a5dc05a5de295daf709..964d51b2c005bb26303b87bc8f3b1fee72d5b6a6 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -10,6 +10,7 @@ namespace App\Entities;
 
 use App\Models\PodcastModel;
 use App\Models\SoundbiteModel;
+use App\Models\EpisodePersonModel;
 use CodeIgniter\Entity;
 use CodeIgniter\I18n\Time;
 use League\CommonMark\CommonMarkConverter;
@@ -76,6 +77,11 @@ class Episode extends Entity
      */
     protected $chapters_url;
 
+    /**
+     * @var \App\Entities\EpisodePerson[]
+     */
+    protected $episode_persons;
+
     /**
      * @var \App\Entities\Soundbite[]
      */
@@ -358,6 +364,29 @@ class Episode extends Entity
             : null;
     }
 
+    /**
+     * Returns the episode's persons
+     *
+     * @return \App\Entities\EpisodePerson[]
+     */
+    public function getEpisodePersons()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Episode must be created before getting persons.'
+            );
+        }
+
+        if (empty($this->episode_persons)) {
+            $this->episode_persons = (new EpisodePersonModel())->getPersonsByEpisodeId(
+                $this->podcast_id,
+                $this->id
+            );
+        }
+
+        return $this->episode_persons;
+    }
+
     /**
      * Returns the episode’s soundbites
      *
diff --git a/app/Entities/EpisodePerson.php b/app/Entities/EpisodePerson.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c0a6388bdc30e58e222366baf95b12fc9295571
--- /dev/null
+++ b/app/Entities/EpisodePerson.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use CodeIgniter\Entity;
+use App\Models\PersonModel;
+
+class EpisodePerson extends Entity
+{
+    /**
+     * @var \App\Entities\Person
+     */
+    protected $person;
+
+    protected $casts = [
+        'id' => 'integer',
+        'podcast_id' => 'integer',
+        'episode_id' => 'integer',
+        'person_id' => 'integer',
+        'person_group' => '?string',
+        'person_role' => '?string',
+    ];
+
+    public function getPerson()
+    {
+        return (new PersonModel())->getPersonById(
+            $this->attributes['person_id']
+        );
+    }
+}
diff --git a/app/Entities/Person.php b/app/Entities/Person.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f20885c37138c33bd82258ce533623f13dd9106
--- /dev/null
+++ b/app/Entities/Person.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use CodeIgniter\Entity;
+
+class Person extends Entity
+{
+    /**
+     * @var \App\Entities\Image
+     */
+    protected $image;
+
+    protected $casts = [
+        'id' => 'integer',
+        'full_name' => 'string',
+        'unique_name' => 'string',
+        'information_url' => '?string',
+        'image_uri' => 'string',
+        'created_by' => 'integer',
+        'updated_by' => 'integer',
+    ];
+
+    /**
+     * Saves a picture in `public/media/~person/`
+     *
+     * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
+     *
+     */
+    public function setImage($image = null)
+    {
+        if ($image) {
+            helper('media');
+
+            $this->attributes['image_uri'] = save_podcast_media(
+                $image,
+                '~person',
+                $this->attributes['unique_name']
+            );
+            $this->image = new \App\Entities\Image(
+                $this->attributes['image_uri']
+            );
+            $this->image->saveSizes();
+        }
+
+        return $this;
+    }
+
+    public function getImage()
+    {
+        return new \App\Entities\Image($this->attributes['image_uri']);
+    }
+}
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 8782a0a7d69db86590b87db0e3655898c21d6243..f35cd75954a0f626e8a37e7ab52f1fbd1b60e994 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -11,6 +11,7 @@ namespace App\Entities;
 use App\Models\CategoryModel;
 use App\Models\EpisodeModel;
 use App\Models\PlatformModel;
+use App\Models\PodcastPersonModel;
 use CodeIgniter\Entity;
 use App\Models\UserModel;
 use League\CommonMark\CommonMarkConverter;
@@ -32,6 +33,11 @@ class Podcast extends Entity
      */
     protected $episodes;
 
+    /**
+     * @var \App\Entities\PodcastPerson[]
+     */
+    protected $podcast_persons;
+
     /**
      * @var \App\Entities\Category
      */
@@ -167,6 +173,28 @@ class Podcast extends Entity
         return $this->episodes;
     }
 
+    /**
+     * Returns the podcast's persons
+     *
+     * @return \App\Entities\PodcastPerson[]
+     */
+    public function getPodcastPersons()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcast must be created before getting persons.'
+            );
+        }
+
+        if (empty($this->podcast_persons)) {
+            $this->podcast_persons = (new PodcastPersonModel())->getPersonsByPodcastId(
+                $this->id
+            );
+        }
+
+        return $this->podcast_persons;
+    }
+
     /**
      * Returns the podcast category entity
      *
diff --git a/app/Entities/PodcastPerson.php b/app/Entities/PodcastPerson.php
new file mode 100644
index 0000000000000000000000000000000000000000..95dec77c5162b797eaa7f2cacfcec30d66c13ce5
--- /dev/null
+++ b/app/Entities/PodcastPerson.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use CodeIgniter\Entity;
+use App\Models\PersonModel;
+
+class PodcastPerson extends Entity
+{
+    /**
+     * @var \App\Entities\Person
+     */
+    protected $person;
+
+    protected $casts = [
+        'id' => 'integer',
+        'podcast_id' => 'integer',
+        'person_id' => 'integer',
+        'person_group' => '?string',
+        'person_role' => '?string',
+    ];
+
+    public function getPerson()
+    {
+        return (new PersonModel())->getPersonById(
+            $this->attributes['person_id']
+        );
+    }
+}
diff --git a/app/Helpers/page_helper.php b/app/Helpers/page_helper.php
index 74b64220d61fe9dd63050f1464ce302ba08b310d..5dda5b387a35893b1c660b5e7298e96ebea15799 100644
--- a/app/Helpers/page_helper.php
+++ b/app/Helpers/page_helper.php
@@ -20,6 +20,9 @@ function render_page_links($class = null)
     $links = anchor(route_to('home'), lang('Common.home'), [
         'class' => 'px-2 underline hover:no-underline',
     ]);
+    $links .= anchor(route_to('credits'), lang('Person.credits'), [
+        'class' => 'px-2 underline hover:no-underline',
+    ]);
     foreach ($pages as $page) {
         $links .= anchor($page->link, $page->title, [
             'class' => 'px-2 underline hover:no-underline',
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index bfc1798493ef5d1af700532f444247e9a4227848..69a08539087f31593bdba637bd498c9212062912 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -68,18 +68,14 @@ function get_rss_feed($podcast, $serviceSlug = '')
     if (!empty($podcast->location_name)) {
         $locationElement = $channel->addChild(
             'location',
-            null,
+            htmlspecialchars($podcast->location_name),
             $podcast_namespace
         );
-        $locationElement->addAttribute(
-            'name',
-            htmlspecialchars($podcast->location_name)
-        );
         if (!empty($podcast->location_geo)) {
             $locationElement->addAttribute('geo', $podcast->location_geo);
         }
         if (!empty($podcast->location_osmid)) {
-            $locationElement->addAttribute('osmid', $podcast->location_osmid);
+            $locationElement->addAttribute('osm', $podcast->location_osmid);
         }
     }
     if (!empty($podcast->payment_pointer)) {
@@ -105,7 +101,7 @@ function get_rss_feed($podcast, $serviceSlug = '')
         )
         ->addAttribute('owner', $podcast->owner_email);
     if (!empty($podcast->imported_feed_url)) {
-        $channel->addChildWithCDATA(
+        $channel->addChild(
             'previousUrl',
             $podcast->imported_feed_url,
             $podcast_namespace
@@ -169,6 +165,51 @@ function get_rss_feed($podcast, $serviceSlug = '')
         }
     }
 
+    foreach ($podcast->podcast_persons as $podcastPerson) {
+        $podcastPersonElement = $channel->addChild(
+            'person',
+            htmlspecialchars($podcastPerson->person->full_name),
+            $podcast_namespace
+        );
+        if (
+            !empty($podcastPerson->person_role) &&
+            !empty($podcastPerson->person_group)
+        ) {
+            $podcastPersonElement->addAttribute(
+                'role',
+                htmlspecialchars(
+                    lang(
+                        "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
+                        [],
+                        'en'
+                    )
+                )
+            );
+        }
+        if (!empty($podcastPerson->person_group)) {
+            $podcastPersonElement->addAttribute(
+                'group',
+                htmlspecialchars(
+                    lang(
+                        "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
+                        [],
+                        'en'
+                    )
+                )
+            );
+        }
+        $podcastPersonElement->addAttribute(
+            'img',
+            $podcastPerson->person->image->large_url
+        );
+        if (!empty($podcastPerson->person->information_url)) {
+            $podcastPersonElement->addAttribute(
+                'href',
+                $podcastPerson->person->information_url
+            );
+        }
+    }
+
     // set main category first, then other categories as apple
     add_category_tag($channel, $podcast->category);
     foreach ($podcast->other_categories as $other_category) {
@@ -222,21 +263,14 @@ function get_rss_feed($podcast, $serviceSlug = '')
         if (!empty($episode->location_name)) {
             $locationElement = $item->addChild(
                 'location',
-                null,
+                htmlspecialchars($episode->location_name),
                 $podcast_namespace
             );
-            $locationElement->addAttribute(
-                'name',
-                htmlspecialchars($episode->location_name)
-            );
             if (!empty($episode->location_geo)) {
                 $locationElement->addAttribute('geo', $episode->location_geo);
             }
             if (!empty($episode->location_osmid)) {
-                $locationElement->addAttribute(
-                    'osmid',
-                    $episode->location_osmid
-                );
+                $locationElement->addAttribute('osm', $episode->location_osmid);
             }
         }
         $item->addChildWithCDATA('description', $episode->description_html);
@@ -312,6 +346,51 @@ function get_rss_feed($podcast, $serviceSlug = '')
             $soundbiteElement->addAttribute('duration', $soundbite->duration);
         }
 
+        foreach ($episode->episode_persons as $episodePerson) {
+            $episodePersonElement = $item->addChild(
+                'person',
+                htmlspecialchars($episodePerson->person->full_name),
+                $podcast_namespace
+            );
+            if (
+                !empty($episodePerson->person_role) &&
+                !empty($episodePerson->person_group)
+            ) {
+                $episodePersonElement->addAttribute(
+                    'role',
+                    htmlspecialchars(
+                        lang(
+                            "PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
+                            [],
+                            'en'
+                        )
+                    )
+                );
+            }
+            if (!empty($episodePerson->person_group)) {
+                $episodePersonElement->addAttribute(
+                    'group',
+                    htmlspecialchars(
+                        lang(
+                            "PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
+                            [],
+                            'en'
+                        )
+                    )
+                );
+            }
+            $episodePersonElement->addAttribute(
+                'img',
+                $episodePerson->person->image->large_url
+            );
+            if (!empty($episodePerson->person->information_url)) {
+                $episodePersonElement->addAttribute(
+                    'href',
+                    $episodePerson->person->information_url
+                );
+            }
+        }
+
         $episode->is_blocked &&
             $item->addChild('block', 'Yes', $itunes_namespace);
     }
diff --git a/app/Language/en/AdminNavigation.php b/app/Language/en/AdminNavigation.php
index d9c2ada029b5d50c3d98ea26814e3b339a873fba..aa36aabfd2514fa52f6f6ed2dad67dbc3e9a792f 100644
--- a/app/Language/en/AdminNavigation.php
+++ b/app/Language/en/AdminNavigation.php
@@ -14,6 +14,9 @@ return [
     'podcast-list' => 'All podcasts',
     'podcast-create' => 'New podcast',
     'podcast-import' => 'Import a podcast',
+    'persons' => 'Persons',
+    'person-list' => 'All persons',
+    'person-create' => 'New person',
     'users' => 'Users',
     'user-list' => 'All users',
     'user-create' => 'New user',
diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php
index 27301ab73a22642f900756c65809c9290700520b..03db0bce7f4e22e1c9257f41788613046373f423 100644
--- a/app/Language/en/Breadcrumb.php
+++ b/app/Language/en/Breadcrumb.php
@@ -16,6 +16,7 @@ return [
     'add' => 'add',
     'new' => 'new',
     'edit' => 'edit',
+    'persons' => 'persons',
     'users' => 'users',
     'my-account' => 'my account',
     'change-password' => 'change password',
diff --git a/app/Language/en/Person.php b/app/Language/en/Person.php
new file mode 100644
index 0000000000000000000000000000000000000000..3d504b2ad47271a83d3080d9128122ea7bfdb8fe
--- /dev/null
+++ b/app/Language/en/Person.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+return [
+    'persons' => 'Persons',
+    'all_persons' => 'All persons',
+    'no_person' => 'Nobody found!',
+    'create' => 'Create a person',
+    'view' => 'View person',
+    'edit' => 'Edit person',
+    'delete' => 'Delete person',
+    'form' => [
+        'identity_section_title' => 'Identity',
+        'identity_section_subtitle' => 'Who is working on the podcast',
+        'full_name' => 'Full name',
+        'full_name_hint' => 'This is the full name or alias of the person.',
+        'unique_name' => 'Unique name',
+        'unique_name_hint' => 'Used for URLs',
+        'information_url' => 'Information URL',
+        'information_url_hint' =>
+            'Url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
+        'image' => 'Picture, avatar, image',
+        'image_size_hint' =>
+            'Image must be squared with at least 400px wide and tall.',
+        'submit_create' => 'Create person',
+        'submit_edit' => 'Save person',
+    ],
+    'podcast_form' => [
+        'title' => 'Manage persons',
+        'manage_section_title' => 'Management',
+        'manage_section_subtitle' => 'Remove persons from this podcast',
+        'add_section_title' => 'Add persons to this podcast',
+        'add_section_subtitle' => 'You may pick several persons and roles.',
+        'person' => 'Persons',
+        'person_hint' =>
+            'You may select one or several persons with the same roles. You need to create the persons first.',
+        'group_role' => 'Groups and roles',
+        'group_role_hint' =>
+            'You may select none, one or several groups and roles for a person.',
+        'submit_add' => 'Add person(s)',
+        'remove' => 'Remove',
+    ],
+    'episode_form' => [
+        'title' => 'Manage persons',
+        'manage_section_title' => 'Management',
+        'manage_section_subtitle' => 'Remove persons from this episode',
+        'add_section_title' => 'Add persons to this episode',
+        'add_section_subtitle' => 'You may pick several persons and roles',
+        'person' => 'Persons',
+        'person_hint' =>
+            'You may select one or several persons with the same roles. You need to create the persons first.',
+        'group_role' => 'Groups and roles',
+        'group_role_hint' =>
+            'You may select none, one or several groups and roles for a person.',
+        'submit_add' => 'Add person(s)',
+        'remove' => 'Remove',
+    ],
+    'credits' => 'Credits',
+];
diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php
index 86f7b4c9bd635710d8469fac454ee6dcc398a48e..f7c87572990259d40466396c91cf0711fdc3ac97 100644
--- a/app/Language/en/Podcast.php
+++ b/app/Language/en/Podcast.php
@@ -27,7 +27,8 @@ return [
         'image' => 'Cover image',
         'title' => 'Title',
         'name' => 'Name',
-        'name_hint' => 'Used for generating the podcast URL.',
+        'name_hint' =>
+            'Used for generating the podcast URL. Uppercase, lowercase, numbers and underscores are accepted.',
         'type' => [
             'label' => 'Type',
             'hint' =>
diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php
index 048ca8be92b2d16539e4bbf63d07ec419e8f5d0b..49062fe30e9cb0e012133b799281231e55b2c206 100644
--- a/app/Language/en/PodcastNavigation.php
+++ b/app/Language/en/PodcastNavigation.php
@@ -15,6 +15,8 @@ return [
     'episode-list' => 'All episodes',
     'episode-create' => 'New episode',
     'analytics' => 'Analytics',
+    'persons' => 'Persons',
+    'podcast-person-manage' => 'Manage persons',
     'contributors' => 'Contributors',
     'contributor-list' => 'All contributors',
     'contributor-add' => 'Add contributor',
diff --git a/app/Language/fr/AdminNavigation.php b/app/Language/fr/AdminNavigation.php
index ea79018dd7a600a01332e187ecfea88ffb30d8ab..b22523f359dd12238678186b074773a82b90fbe9 100644
--- a/app/Language/fr/AdminNavigation.php
+++ b/app/Language/fr/AdminNavigation.php
@@ -14,6 +14,9 @@ return [
     'podcast-list' => 'Tous les podcasts',
     'podcast-create' => 'Créer un podcast',
     'podcast-import' => 'Importer un podcast',
+    'persons' => 'Intervenants',
+    'person-list' => 'Tous les intervenants',
+    'person-create' => 'Nouvel intervenant',
     'users' => 'Utilisateurs',
     'user-list' => 'Tous les utilisateurs',
     'user-create' => 'Créer un utilisateur',
diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php
index 71d8c331d9303395334e10d84c1d0a94f89ba487..961d403c081fd1fbf64e571dbaca19a51c692132 100644
--- a/app/Language/fr/Breadcrumb.php
+++ b/app/Language/fr/Breadcrumb.php
@@ -16,6 +16,7 @@ return [
     'add' => 'ajouter',
     'new' => 'créer',
     'edit' => 'modifier',
+    'persons' => 'intervenants',
     'users' => 'utilisateurs',
     'my-account' => 'mon compte',
     'change-password' => 'changer le mot de passe',
diff --git a/app/Language/fr/Person.php b/app/Language/fr/Person.php
new file mode 100644
index 0000000000000000000000000000000000000000..aef667f0ac8e143e3cf73d1c2466cfe0440729d6
--- /dev/null
+++ b/app/Language/fr/Person.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+return [
+    'persons' => 'Intervenants',
+    'all_persons' => 'Tous les intervenants',
+    'no_person' => 'Aucun intervenant trouvé !',
+    'create' => 'Créer un intervenant',
+    'view' => 'Voir l’intervenant',
+    'edit' => 'Modifier l’intervenant',
+    'delete' => 'Supprimer l’intervenant',
+    'form' => [
+        'identity_section_title' => 'Identité',
+        'identity_section_subtitle' => 'Qui intervient sur le podcast',
+        'full_name' => 'Nom complet',
+        'full_name_hint' => 'Le nom complet ou le pseudonyme de l’intervenant',
+        'unique_name' => 'Nom unique',
+        'unique_name_hint' => 'Utilisé pour les URLs',
+        'information_url' => 'Adresse d’information',
+        'information_url_hint' =>
+            'URL pointant vers des informations relatives à l’intervenant, telle qu’une page personnelle ou une page de profil sur une plateforme tierce.',
+        'image' => 'Photo, avatar, image',
+        'image_size_hint' =>
+            'L’image doit être carrée et avoir au moins 400px de largeur et de hauteur.',
+        'submit_create' => 'Créer l’intervenant',
+        'submit_edit' => 'Enregistrer l’intervenant',
+    ],
+    'podcast_form' => [
+        'title' => 'Gérer les intervenants',
+        'manage_section_title' => 'Gestion',
+        'manage_section_subtitle' => 'Retirer des intervenants de ce podcast',
+        'add_section_title' => 'Ajouter des intervenants à ce podcast',
+        'add_section_subtitle' =>
+            'Vous pouvez sélectionner plusieurs intervenants et rôles.',
+        'person' => 'Intervenants',
+        'person_hint' =>
+            'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.',
+        'group_role' => 'Groupes et rôles',
+        'group_role_hint' =>
+            'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.',
+        'submit_add' => 'Ajouter un/des intervenant(s)',
+        'remove' => 'Retirer',
+    ],
+    'episode_form' => [
+        'title' => 'Gérer les intervenants',
+        'manage_section_title' => 'Gestion',
+        'manage_section_subtitle' => 'Retirer des intervenants de cet épisode',
+        'add_section_title' => 'Ajouter des intervenants à cet épisode',
+        'add_section_subtitle' =>
+            'Vous pouvez sélectionner plusieurs intervenants et rôles.',
+        'person' => 'Intervenants',
+        'person_hint' =>
+            'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.',
+        'group_role' => 'Groupes et rôles',
+        'group_role_hint' =>
+            'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.',
+        'submit_add' => 'Ajouter un/des intervenant(s)',
+        'remove' => 'Retirer',
+    ],
+    'credits' => 'Crédits',
+];
diff --git a/app/Language/fr/Podcast.php b/app/Language/fr/Podcast.php
index 49131cbf5449ed344965dcf510a1f1e04d47d1f7..20672a4cddd1ee5810aadd130076abe96e493a77 100644
--- a/app/Language/fr/Podcast.php
+++ b/app/Language/fr/Podcast.php
@@ -28,7 +28,8 @@ return [
         'image' => 'Image de couverture',
         'title' => 'Titre',
         'name' => 'Nom',
-        'name_hint' => 'Utilisé pour l’adresse du podcast.',
+        'name_hint' =>
+            'Utilisé pour l’adresse du podcast. Les majuscules, les minuscules, les chiffres et le caractère souligné « _ » sont acceptés.',
         'type' => [
             'label' => 'Type',
             'hint' =>
diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php
index 5ac59cd290b45537f1bdfa0434b7b191c002bc7f..1b5414bc6959f6e7fb1d14cd06b708efae1dac25 100644
--- a/app/Language/fr/PodcastNavigation.php
+++ b/app/Language/fr/PodcastNavigation.php
@@ -15,6 +15,8 @@ return [
     'episode-list' => 'Tous les épisodes',
     'episode-create' => 'Créer un épisode',
     'analytics' => 'Mesures d’audience',
+    'persons' => 'Intervenants',
+    'podcast-person-manage' => 'Gestion des intervenants',
     'contributors' => 'Contributeurs',
     'contributor-list' => 'Tous les contributeurs',
     'contributor-add' => 'Ajouter un contributeur',
diff --git a/app/Models/CreditModel.php b/app/Models/CreditModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..00121757bfe1ce19f11c97b0b56d89ac2e44eaf1
--- /dev/null
+++ b/app/Models/CreditModel.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class CreditModel extends Model
+{
+    protected $table = 'credits';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\Credit::class;
+}
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index a28d29d3499387851cd46923d71eea813b76092c..eacbac9b3c2e91b4c4614fbde6126beda2d49961 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -89,6 +89,26 @@ class EpisodeModel extends Model
         return $found;
     }
 
+    public function getEpisodeById($podcastId, $episodeId)
+    {
+        if (!($found = cache("podcast{$podcastId}_episode{$episodeId}"))) {
+            $found = $this->where([
+                'podcast_id' => $podcastId,
+                'id' => $episodeId,
+            ])
+                ->where('published_at <=', 'NOW()')
+                ->first();
+
+            cache()->save(
+                "podcast{$podcastId}_episode{$episodeId}",
+                $found,
+                DECADE
+            );
+        }
+
+        return $found;
+    }
+
     /**
      * Returns the previous episode based on episode ordering
      */
@@ -334,7 +354,7 @@ class EpisodeModel extends Model
         return $data;
     }
 
-    protected function clearCache(array $data)
+    public function clearCache(array $data)
     {
         $episodeModel = new EpisodeModel();
         $episode = (new EpisodeModel())->find(
@@ -366,6 +386,7 @@ class EpisodeModel extends Model
             cache()->delete(
                 "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}"
             );
+            cache()->delete("credits_{$locale}");
         }
 
         foreach ($years as $year) {
diff --git a/app/Models/EpisodePersonModel.php b/app/Models/EpisodePersonModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ed80d1e06f58cdababde8e781c36a0b476e7261
--- /dev/null
+++ b/app/Models/EpisodePersonModel.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class EpisodePersonModel extends Model
+{
+    protected $table = 'episodes_persons';
+    protected $primaryKey = 'id';
+
+    protected $allowedFields = [
+        'id',
+        'podcast_id',
+        'episode_id',
+        'person_id',
+        'person_group',
+        'person_role',
+    ];
+
+    protected $returnType = \App\Entities\EpisodePerson::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    protected $validationRules = [
+        'episode_id' => 'required',
+        'person_id' => 'required',
+    ];
+    protected $validationMessages = [];
+
+    protected $afterInsert = ['clearCache'];
+    protected $beforeDelete = ['clearCache'];
+
+    public function getPersonsByEpisodeId($podcastId, $episodeId)
+    {
+        if (
+            !($found = cache(
+                "podcast{$podcastId}_episodes{$episodeId}_persons"
+            ))
+        ) {
+            $found = $this->select('episodes_persons.*')
+                ->where('episode_id', $episodeId)
+                ->join(
+                    'persons',
+                    'person_id=persons.id'
+                )
+                ->orderby('full_name')
+                ->findAll();
+
+            cache()->save(
+                "podcast{$podcastId}_episodes{$episodeId}_persons",
+                $found,
+                DECADE
+            );
+        }
+        return $found;
+    }
+
+    /**
+     * Add persons to episode
+     *
+     * @param int podcastId
+     * @param int $episodeId
+     * @param array $persons
+     * @param array $groups_roles
+     *
+     * @return integer|false Number of rows inserted or FALSE on failure
+     */
+    public function addEpisodePersons(
+        $podcastId,
+        $episodeId,
+        $persons,
+        $groups_roles
+    ) {
+        if (!empty($persons)) {
+            $this->clearCache([
+                'id' => [
+                    'podcast_id' => $podcastId,
+                    'episode_id' => $episodeId,
+                ],
+            ]);
+            $data = [];
+            foreach ($persons as $person) {
+                if ($groups_roles) {
+                    foreach ($groups_roles as $group_role) {
+                        $group_role = explode(',', $group_role);
+                        $data[] = [
+                            'podcast_id' => $podcastId,
+                            'episode_id' => $episodeId,
+                            'person_id' => $person,
+                            'person_group' => $group_role[0],
+                            'person_role' => $group_role[1],
+                        ];
+                    }
+                } else {
+                    $data[] = [
+                        'podcast_id' => $podcastId,
+                        'episode_id' => $episodeId,
+                        'person_id' => $person,
+                    ];
+                }
+            }
+            return $this->insertBatch($data);
+        }
+        return 0;
+    }
+
+    public function removeEpisodePersons(
+        $podcastId,
+        $episodeId,
+        $episodePersonId
+    ) {
+        return $this->delete([
+            'podcast_id' => $podcastId,
+            'episode_id' => $episodeId,
+            'id' => $episodePersonId,
+        ]);
+    }
+
+    protected function clearCache(array $data)
+    {
+        $podcastId = null;
+        $episodeId = null;
+        if (
+            isset($data['id']['podcast_id']) &&
+            isset($data['id']['episode_id'])
+        ) {
+            $podcastId = $data['id']['podcast_id'];
+            $episodeId = $data['id']['episode_id'];
+        } else {
+            $episodePerson = (new EpisodePersonModel())->find(
+                is_array($data['id']) ? $data['id']['id'] : $data['id']
+            );
+            $podcastId = $episodePerson->podcast_id;
+            $episodeId = $episodePerson->episode_id;
+        }
+
+        cache()->delete("podcast{$podcastId}_episodes{$episodeId}_persons");
+        (new EpisodeModel())->clearCache(['id' => $episodeId]);
+
+        return $data;
+    }
+}
diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac8661c8d11a2c4b7a2a6e4f762870f0d1e48dcd
--- /dev/null
+++ b/app/Models/PersonModel.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class PersonModel extends Model
+{
+    protected $table = 'persons';
+    protected $primaryKey = 'id';
+
+    protected $allowedFields = [
+        'id',
+        'full_name',
+        'unique_name',
+        'information_url',
+        'image_uri',
+        'created_by',
+        'updated_by',
+    ];
+
+    protected $returnType = \App\Entities\Person::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = true;
+
+    protected $validationRules = [
+        'full_name' => 'required',
+        'unique_name' =>
+            'required|regex_match[/^[a-z0-9\-]{1,191}$/]|is_unique[persons.unique_name,id,{id}]',
+        'image_uri' => 'required',
+        'created_by' => 'required',
+        'updated_by' => 'required',
+    ];
+    protected $validationMessages = [];
+
+    // clear cache before update if by any chance, the person name changes, so will the person link
+    protected $afterInsert = ['clearCache'];
+    protected $beforeUpdate = ['clearCache'];
+    protected $beforeDelete = ['clearCache'];
+
+    public function getPersonById($personId)
+    {
+        if (!($found = cache("person{$personId}"))) {
+            $found = $this->find($personId);
+            cache()->save("person{$personId}", $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    public function getPerson($fullName)
+    {
+        return $this->where('full_name', $fullName)->first();
+    }
+
+    public function createPerson($fullName, $informationUrl, $image)
+    {
+        $person = new \App\Entities\Person([
+            'full_name' => $fullName,
+            'unique_name' => slugify($fullName),
+            'information_url' => $informationUrl,
+            'image' => download_file($image),
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+        ]);
+        return $this->insert($person);
+    }
+
+    public function getPersonOptions()
+    {
+        $options = [];
+
+        if (!($options = cache('person_options'))) {
+            $options = array_reduce(
+                $this->select('`id`, `full_name`')
+                    ->orderBy('`full_name`', 'ASC')
+                    ->findAll(),
+                function ($result, $person) {
+                    $result[$person->id] = $person->full_name;
+                    return $result;
+                },
+                []
+            );
+            cache()->save('person_options', $options, DECADE);
+        }
+
+        return $options;
+    }
+
+    public function getTaxonomyOptions()
+    {
+        $options = [];
+        $locale = service('request')->getLocale();
+        if (!($options = cache("taxonomy_options_{$locale}"))) {
+            foreach (lang('PersonsTaxonomy.persons') as $group_key => $group) {
+                foreach ($group['roles'] as $role_key => $role) {
+                    $options[
+                        "$group_key,$role_key"
+                    ] = "{$group['label']}  â–¸  {$role['label']}";
+                }
+            }
+
+            cache()->save("taxonomy_options_{$locale}", $options, DECADE);
+        }
+
+        return $options;
+    }
+
+    protected function clearCache(array $data)
+    {
+        $person = (new PersonModel())->getPersonById(
+            is_array($data['id']) ? $data['id'][0] : $data['id']
+        );
+
+        cache()->delete('person_options');
+        cache()->delete("person{$person->id}");
+        cache()->delete("user{$person->created_by}_persons");
+
+        $supportedLocales = config('App')->supportedLocales;
+        // clear cache for every credit page
+        foreach ($supportedLocales as $locale) {
+            cache()->delete("credit_{$locale}");
+        }
+
+        return $data;
+    }
+}
diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php
index f13586dcbe99415ed800da069f8792a22e47886c..827c4de1fff0957b1254d7add4127d7357c2c5db 100644
--- a/app/Models/PlatformModel.php
+++ b/app/Models/PlatformModel.php
@@ -16,14 +16,20 @@ use CodeIgniter\Model;
 class PlatformModel extends Model
 {
     protected $table = 'platforms';
-    protected $primaryKey = 'id';
+    protected $primaryKey = 'slug';
 
-    protected $allowedFields = ['slug', 'label', 'home_url', 'submit_url'];
+    protected $allowedFields = [
+        'slug',
+        'type',
+        'label',
+        'home_url',
+        'submit_url',
+    ];
 
     protected $returnType = \App\Entities\Platform::class;
     protected $useSoftDeletes = false;
 
-    protected $useTimestamps = true;
+    protected $useTimestamps = false;
 
     public function getPlatforms()
     {
@@ -37,26 +43,32 @@ class PlatformModel extends Model
         return $found;
     }
 
-    public function getOrCreatePlatform($slug, $platformType)
+    public function getPlatform($slug)
     {
-        if (!($found = cache("platforms_$slug"))) {
+        if (!($found = cache("platform_$slug"))) {
             $found = $this->where('slug', $slug)->first();
-            if (!$found) {
-                $data = [
-                    'slug' => $slug,
-                    'type' => $platformType,
-                    'label' => $slug,
-                    'home_url' => '',
-                    'submit_url' => null,
-                ];
-                $this->insert($data);
-                $found = $this->where('slug', $slug)->first();
-            }
-            cache()->save("platforms_$slug", $found, DECADE);
+            cache()->save("platform_$slug", $found, DECADE);
         }
         return $found;
     }
 
+    public function createPlatform(
+        $slug,
+        $type,
+        $label,
+        $homeUrl,
+        $submitUrl = null
+    ) {
+        $data = [
+            'slug' => $slug,
+            'type' => $type,
+            'label' => $label,
+            'home_url' => $homeUrl,
+            'submit_url' => $submitUrl,
+        ];
+        return $this->insert($data, false);
+    }
+
     public function getPlatformsWithLinks($podcastId, $platformType)
     {
         if (
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 7e401fd16f9e6df701d8062c4034335e95e9a134..4bfed81d2cf810f03a177f82ea69a0d885d3ac45 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * @copyright  2020 Podlibre
+ * @copyright  2021 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
  */
@@ -170,7 +170,7 @@ class PodcastModel extends Model
             : false;
     }
 
-    protected function clearCache(array $data)
+    public function clearCache(array $data)
     {
         $podcast = (new PodcastModel())->getPodcastById(
             is_array($data['id']) ? $data['id'][0] : $data['id']
@@ -195,6 +195,10 @@ class PodcastModel extends Model
                 );
             }
         }
+        // clear cache for every credit page
+        foreach ($supportedLocales as $locale) {
+            cache()->delete("credits_{$locale}");
+        }
 
         // delete episode lists cache per year / season
         // and localized pages
diff --git a/app/Models/PodcastPersonModel.php b/app/Models/PodcastPersonModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..8268cf0ebd2f544b2e1975f25d30c96db0c02d42
--- /dev/null
+++ b/app/Models/PodcastPersonModel.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class PodcastPersonModel extends Model
+{
+    protected $table = 'podcasts_persons';
+    protected $primaryKey = 'id';
+
+    protected $allowedFields = [
+        'id',
+        'podcast_id',
+        'person_id',
+        'person_group',
+        'person_role',
+    ];
+
+    protected $returnType = \App\Entities\PodcastPerson::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    protected $validationRules = [
+        'podcast_id' => 'required',
+        'person_id' => 'required',
+    ];
+    protected $validationMessages = [];
+
+    protected $afterInsert = ['clearCache'];
+    protected $beforeDelete = ['clearCache'];
+
+    public function getPersonsByPodcastId($podcastId)
+    {
+        if (!($found = cache("podcast{$podcastId}_persons"))) {
+            $found = $this->select('podcasts_persons.*')
+                ->where('podcast_id', $podcastId)
+                ->join(
+                    'persons',
+                    'person_id=persons.id'
+                )
+                ->orderby('full_name')
+                ->findAll();
+
+            cache()->save("podcast{$podcastId}_persons", $found, DECADE);
+        }
+        return $found;
+    }
+
+    /**
+     * Add persons to podcast
+     *
+     * @param int $podcastId
+     * @param array $persons
+     * @param array $groups_roles
+     *
+     * @return integer Number of rows inserted or FALSE on failure
+     */
+    public function addPodcastPersons($podcastId, $persons, $groups_roles)
+    {
+        if (!empty($persons)) {
+            $this->clearCache(['id' => ['podcast_id' => $podcastId]]);
+            $data = [];
+            foreach ($persons as $person) {
+                if ($groups_roles) {
+                    foreach ($groups_roles as $group_role) {
+                        $group_role = explode(',', $group_role);
+                        $data[] = [
+                            'podcast_id' => $podcastId,
+                            'person_id' => $person,
+                            'person_group' => $group_role[0],
+                            'person_role' => $group_role[1],
+                        ];
+                    }
+                } else {
+                    $data[] = [
+                        'podcast_id' => $podcastId,
+                        'person_id' => $person,
+                    ];
+                }
+            }
+            return $this->insertBatch($data);
+        }
+        return 0;
+    }
+
+    public function removePodcastPersons($podcastId, $podcastPersonId)
+    {
+        return $this->delete([
+            'podcast_id' => $podcastId,
+            'id' => $podcastPersonId,
+        ]);
+    }
+
+    protected function clearCache(array $data)
+    {
+        $podcastId = null;
+        if (isset($data['id']['podcast_id'])) {
+            $podcastId = $data['id']['podcast_id'];
+        } else {
+            $person = (new PodcastPersonModel())->find(
+                is_array($data['id']) ? $data['id']['id'] : $data['id']
+            );
+            $podcastId = $person->podcast_id;
+        }
+
+        cache()->delete("podcast{$podcastId}_persons");
+        (new PodcastModel())->clearCache(['id' => $podcastId]);
+
+        return $data;
+    }
+}
diff --git a/app/Views/_assets/icons/folder-user.svg b/app/Views/_assets/icons/folder-user.svg
new file mode 100644
index 0000000000000000000000000000000000000000..590e6aa19abc431a1edf746bb8630f1edb4efab2
--- /dev/null
+++ b/app/Views/_assets/icons/folder-user.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12.414 5H21a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7.414l2 2zM4 5v14h16V7h-8.414l-2-2H4zm4 13a4 4 0 1 1 8 0H8zm4-5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/images/platforms/podcasting/breaker.svg b/app/Views/_assets/images/platforms/podcasting/breaker.svg
new file mode 100644
index 0000000000000000000000000000000000000000..27eeadd7408cdbdf523848b3d8f6eb020ab90625
--- /dev/null
+++ b/app/Views/_assets/images/platforms/podcasting/breaker.svg
@@ -0,0 +1,11 @@
+<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
+ <rect width="300" height="300" rx="67" fill="#f2f8ff"/>
+ <g transform="matrix(1.36 0 0 1.36 -45.282 22.882)">
+  <path d="m133.88 120.47c4.08 12.49 10.64 23.85 19.12 33.5 15.88-2.06 29.85-10.22 39.46-22.06-7.84 2.07-16.07 3.17-24.56 3.17-6.79 0-13.42-.72-19.81-2.06-6.67-1.36-12.07-6.21-14.21-12.55z" fill="#1269ff"/>
+  <path d="m145.09 154.47c2.68 0 5.32-.17 7.91-.51-8.48-9.65-15.04-21.01-19.12-33.5-.64-1.91-.99-3.95-.99-6.07 0-4.77 1.76-9.12 4.66-12.46-11.1 12.41-19.02 27.71-22.48 44.64 8.85 5.03 19.1 7.9 30.02 7.9z" fill="#5c9dff"/>
+  <path d="m85.78 107.8c4 16.61 14.79 30.57 29.28 38.78 3.47-16.95 11.4-32.28 22.52-44.69 3.48-3.98 8.6-6.49 14.3-6.49 1.27 0 2.5.12 3.7.36-6.36-1.33-12.95-2.04-19.71-2.04-18.35-.01-35.5 5.14-50.09 14.08z" fill="#9ec6ff"/>
+  <path d="m155.59 95.754c-6.36-1.33-12.95-2.04-19.71-2.04-18.36 0-35.51 5.15-50.1 14.09-1.1-4.59-1.69-9.39-1.69-14.33 0-19.02 8.71-36.01 22.35-47.2 29.91 9.03 53.64 32.31 63.39 61.91.09.26.18.52.26.79-.08-.26-.17-.53-.26-.79-2.19-6.29-7.6-11.12-14.24-12.43z" fill="#d1e3ff"/>
+  <path d="m203.08 82.474c0-27-22.49-50-58-50-14.66 0-28.12 5.18-38.64 13.8 30.18 9.12 54.07 32.7 63.64 62.69.56 1.75 1.07 3.62 1.53 5.42 17.34-.38 31.47-14.48 31.47-31.91z" fill="#fff"/>
+  <path d="m151.89 98.394c1.12 0 2.06.09 3.12.3 5.56 1.1 10.12 5.14 11.98 10.43.54 1.7 1.25 4.25 1.7 6l.59 2.31 2.38-.05c18.94-.4 34.43-15.81 34.43-34.91 0-28.93-24.13-53-61-53-15.21.05-29.63 5.58-40.55 14.48-14.31 11.73-23.45 29.56-23.45 49.52.01 5.12.63 10.27 1.78 15.03 4.2 17.43 15.52 32.08 30.71 40.68 9.19 5.18 20.18 8.25 31.51 8.28 2.78 0 5.61-.19 8.29-.53 16.67-2.16 31.32-10.73 41.41-23.14l.66-.81-1.28-4.63-2.47.65c-7.66 2.03-15.51 3.08-23.8 3.07-6.63 0-12.95-.68-19.19-2-5.62-1.13-10.19-5.22-11.99-10.56-.58-1.68-.84-3.24-.83-5.11-.06-4.17 1.37-7.6 3.95-10.53 2.94-3.36 7.25-5.48 12.05-5.48zm-6.8-62.92c34.12 0 55 21.92 55 47 0 14.99-11.56 27.3-26.23 28.78-.39-1.39-.81-2.86-1.17-3.98-9.29-29.1-31.41-51.63-59.87-62.03 9.25-6.28 20.16-9.81 32.27-9.77zm-58 58c0-17.51 7.76-33.21 20.03-43.85 23.34 7.48 42.47 23.45 53.64 44.64-1.45-.64-2.98-1.13-4.58-1.45-6.5-1.35-13.39-2.1-20.3-2.1-17.28.03-33.84 4.58-48 12.4-.53-3.13-.79-6.31-.79-9.64zm60.39 42.48c6.53 1.37 13.47 2.12 20.42 2.12 5.31 0 10.59-.44 15.72-1.25-8.1 7.21-18.23 12.17-29.43 13.94-4.26-5-7.96-10.4-11.05-16.17 1.38.59 2.83 1.05 4.34 1.36zm-.45 15.49c-.64.02-1.29.03-1.94.03-9.73.03-18.59-2.26-26.63-6.45 2.31-10.17 6.25-19.6 11.58-28.14.18 1.57.53 3.13.99 4.52 3.57 10.89 9.08 21.14 16 30.04zm-11.69-51.54c-10.42 11.68-18.29 26.2-22.27 41.94-11.43-7.58-20.01-19.1-23.82-32.62 13.79-8.04 29.5-12.54 46.63-12.51.96 0 1.91.01 2.86.04-1.24.93-2.38 1.98-3.4 3.15z" fill="#003dad"/>
+ </g>
+</svg>
diff --git a/app/Views/_layout.php b/app/Views/_layout.php
index 831f2cb0b682eb1bbea719fcefaef7831c03603e..9f0e6a9d947e504c4451fab1a1f6546576c630ba 100644
--- a/app/Views/_layout.php
+++ b/app/Views/_layout.php
@@ -11,8 +11,8 @@
     <link rel="stylesheet" href="/assets/index.css"/>
 </head>
 
-<body class="flex flex-col min-h-screen mx-auto">
-    <header class="border-b">
+<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
+    <header class="bg-white border-b">
         <div class="container flex items-center justify-between px-2 py-4 mx-auto">
             <a href="<?= route_to('home') ?>" class="text-2xl"><?= isset($page)
     ? $page->title
@@ -22,11 +22,15 @@
     <main class="container flex-1 px-4 py-10 mx-auto">
         <?= $this->renderSection('content') ?>
     </main>
-    <footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t">
-        <?= render_page_links() ?>
-        <small><?= lang('Common.powered_by', [
-            'castopod' =>
-                '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
-        ]) ?></small>
-    </footer>
+    <footer class="px-2 py-4 bg-white border-t">
+        <div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row ">
+            <?= render_page_links('inline-flex mb-4 md:mb-0') ?>
+            <p class="flex flex-col items-center md:items-end">
+                <?= lang('Common.powered_by', [
+                    'castopod' =>
+                        '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
+                ]) ?>
+            </p>
+        </div>
+    </footer>    
 </body>
diff --git a/app/Views/admin/_sidebar.php b/app/Views/admin/_sidebar.php
index b57cccfe2b7dfd38d293292d4212ee028b7c9ef3..069dc74243a1623bbfebf0fbb2c38f8524cde36a 100644
--- a/app/Views/admin/_sidebar.php
+++ b/app/Views/admin/_sidebar.php
@@ -5,6 +5,10 @@ $navigation = [
         'icon' => 'mic',
         'items' => ['podcast-list', 'podcast-create', 'podcast-import'],
     ],
+    'persons' => [
+        'icon' => 'folder-user',
+        'items' => ['person-list', 'person-create'],
+    ],
     'users' => ['icon' => 'group', 'items' => ['user-list', 'user-create']],
     'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']],
 ]; ?>
diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php
index 78f8f4270b32f4ad2dee6ddd1dd60b6d5007951c..6bed3c358aed1680d5bc611ea732bf848eb3e4b2 100644
--- a/app/Views/admin/episode/list.php
+++ b/app/Views/admin/episode/list.php
@@ -61,6 +61,11 @@
                                         $podcast->id,
                                         $episode->id
                                     ) ?>"><?= lang('Episode.edit') ?></a>
+                                    <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                        'episode-person-manage',
+                                        $podcast->id,
+                                        $episode->id
+                                    ) ?>"><?= lang('Person.persons') ?></a>
                                     <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
                                         'soundbites-edit',
                                         $podcast->id,
diff --git a/app/Views/admin/episode/person.php b/app/Views/admin/episode/person.php
new file mode 100644
index 0000000000000000000000000000000000000000..be803b50f4106dfe9af20d5ed448693cf4218231
--- /dev/null
+++ b/app/Views/admin/episode/person.php
@@ -0,0 +1,131 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.episode_form.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Person.episode_form.title') ?> (<?= count($episodePersons) ?>)
+<?= $this->endSection() ?>
+
+<?= $this->section('headerRight') ?>
+<?= button(
+    lang('Person.create'),
+    route_to('person-create'),
+    ['variant' => 'primary', 'iconLeft' => 'add'],
+    ['class' => 'mr-2']
+) ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<?= form_open(route_to('episode-person-edit', $episode->id), [
+    'method' => 'post',
+    'class' => 'flex flex-col',
+]) ?>
+<?= csrf_field() ?>
+
+<?php if ($episodePersons): ?>
+
+<?= form_section(
+    lang('Person.episode_form.manage_section_title'),
+    lang('Person.episode_form.manage_section_subtitle')
+) ?>
+
+
+<?= data_table(
+    [
+        [
+            'header' => lang('Person.episode_form.person'),
+            'cell' => function ($episodePerson) {
+                return '<div class="flex">' .
+                    '<a href="' .
+                    route_to('person-view', $episodePerson->person->id) .
+                    "\"><img src=\"{$episodePerson->person->image->thumbnail_url}\" alt=\"{$episodePerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
+                    '<div class="flex flex-col ml-3">' .
+                    $episodePerson->person->full_name .
+                    ($episodePerson->person_group && $episodePerson->person_role
+                        ? '<span class="text-sm text-gray-600">' .
+                            lang(
+                                "PersonsTaxonomy.persons.{$episodePerson->person_group}.label"
+                            ) .
+                            ' â–¸ ' .
+                            lang(
+                                "PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label"
+                            ) .
+                            '</span>'
+                        : '') .
+                    (empty($episodePerson->person->information_url)
+                        ? ''
+                        : "<a href=\"{$episodePerson->person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
+                            $episodePerson->person->information_url .
+                            '</a>') .
+                    '</div></div>';
+            },
+        ],
+        [
+            'header' => lang('Common.actions'),
+            'cell' => function ($episodePerson) {
+                return button(
+                    lang('Person.episode_form.remove'),
+                    route_to(
+                        'episode-person-remove',
+                        $episodePerson->podcast_id,
+                        $episodePerson->episode_id,
+                        $episodePerson->id
+                    ),
+                    ['variant' => 'danger', 'size' => 'small']
+                );
+            },
+        ],
+    ],
+    $episodePersons
+) ?>
+
+<?= form_section_close() ?>
+<?php endif; ?>
+
+
+<?= form_section(
+    lang('Person.episode_form.add_section_title'),
+    lang('Person.episode_form.add_section_subtitle')
+) ?>
+
+<?= form_label(
+    lang('Person.episode_form.person'),
+    'person',
+    [],
+    lang('Person.episode_form.person_hint')
+) ?>
+<?= form_multiselect('person[]', $personOptions, old('person', []), [
+    'id' => 'person',
+    'class' => 'form-select mb-4',
+    'required' => 'required',
+]) ?>
+
+<?= form_label(
+    lang('Person.episode_form.group_role'),
+    'group_role',
+    [],
+
+    lang('Person.episode_form.group_role_hint'),
+    true
+) ?>
+<?= form_multiselect(
+    'person_group_role[]',
+    $taxonomyOptions,
+    old('person_group_role', []),
+    ['id' => 'person_group_role', 'class' => 'form-select mb-4']
+) ?>
+        
+    
+<?= form_section_close() ?>
+<?= button(
+    lang('Person.episode_form.submit_add'),
+    null,
+    ['variant' => 'primary'],
+    ['type' => 'submit', 'class' => 'self-end']
+) ?> 
+<?= form_close() ?>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php
index a66c8d710cf0a1c6fb3857557b128bccc8250143..08956ec37c83e7c8be6fb287445d167fa5cdd629 100644
--- a/app/Views/admin/episode/view.php
+++ b/app/Views/admin/episode/view.php
@@ -64,6 +64,12 @@
         ['variant' => 'info', 'iconLeft' => 'edit'],
         ['class' => 'mb-4']
     ) ?>
+    <?= button(
+        lang('Person.episode_form.title'),
+        route_to('episode-person-manage', $podcast->id, $episode->id),
+        ['variant' => 'info', 'iconLeft' => 'folder-user'],
+        ['class' => 'mb-4']
+    ) ?>
     <?php if (count($episode->soundbites) > 0): ?>
     <?= data_table(
         [
diff --git a/app/Views/admin/person/create.php b/app/Views/admin/person/create.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ec2c7ee5c6154045cecd2100a2ccf1a6df4a341
--- /dev/null
+++ b/app/Views/admin/person/create.php
@@ -0,0 +1,95 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.create') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Person.create') ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+
+<?= form_open_multipart(route_to('person-create'), [
+    'method' => 'post',
+    'class' => 'flex flex-col',
+]) ?>
+<?= csrf_field() ?>
+
+<?= form_section(
+    lang('Person.form.identity_section_title'),
+    lang('Person.form.identity_section_subtitle')
+) ?>
+
+<?= form_label(
+    lang('Person.form.full_name'),
+    'full_name',
+    [],
+    lang('Person.form.full_name_hint')
+) ?>
+<?= form_input([
+    'id' => 'full_name',
+    'name' => 'full_name',
+    'class' => 'form-input mb-4',
+    'value' => old('full_name'),
+    'required' => 'required',
+    'data-slugify' => 'title',
+]) ?>
+
+<?= form_label(
+    lang('Person.form.unique_name'),
+    'unique_name',
+    [],
+    lang('Person.form.unique_name_hint')
+) ?>
+<?= form_input([
+    'id' => 'unique_name',
+    'name' => 'unique_name',
+    'class' => 'form-input mb-4',
+    'value' => old('unique_name'),
+    'required' => 'required',
+    'data-slugify' => 'slug',
+]) ?>
+
+<?= form_label(
+    lang('Person.form.information_url'),
+    'information_url',
+    [],
+    lang('Person.form.information_url_hint'),
+    true
+) ?>
+<?= form_input([
+    'id' => 'information_url',
+    'name' => 'information_url',
+    'class' => 'form-input mb-4',
+    'value' => old('information_url'),
+]) ?>
+
+<?= form_label(lang('Person.form.image'), 'image') ?>
+<?= form_input([
+    'id' => 'image',
+    'name' => 'image',
+    'class' => 'form-input',
+    'required' => 'required',
+    'type' => 'file',
+    'accept' => '.jpg,.jpeg,.png',
+]) ?>
+<small class="mb-4 text-gray-600"><?= lang(
+    'Person.form.image_size_hint'
+) ?></small>
+
+<?= form_section_close() ?>
+
+<?= button(
+    lang('Person.form.submit_create'),
+    null,
+    ['variant' => 'primary'],
+    ['type' => 'submit', 'class' => 'self-end']
+) ?>
+
+
+<?= form_close() ?>
+
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/person/edit.php b/app/Views/admin/person/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..98a1d629b947f4f1e0228c87ba4d64033c79d225
--- /dev/null
+++ b/app/Views/admin/person/edit.php
@@ -0,0 +1,95 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.edit') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Person.edit') ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+
+<?= form_open_multipart(route_to('person-edit', $person->id), [
+    'method' => 'post',
+    'class' => 'flex flex-col',
+]) ?>
+<?= csrf_field() ?>
+
+<?= form_section(
+    lang('Person.form.identity_section_title'),
+    lang('Person.form.identity_section_subtitle') .
+        "<img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-32 h-32 mt-3 rounded\" />"
+) ?>
+
+<?= form_label(
+    lang('Person.form.full_name'),
+    'full_name',
+    [],
+    lang('Person.form.full_name_hint')
+) ?>
+<?= form_input([
+    'id' => 'full_name',
+    'name' => 'full_name',
+    'class' => 'form-input mb-4',
+    'value' => old('full_name', $person->full_name),
+    'required' => 'required',
+    'data-slugify' => 'title',
+]) ?>
+
+<?= form_label(
+    lang('Person.form.unique_name'),
+    'unique_name',
+    [],
+    lang('Person.form.unique_name_hint')
+) ?>
+<?= form_input([
+    'id' => 'unique_name',
+    'name' => 'unique_name',
+    'class' => 'form-input mb-4',
+    'value' => old('unique_name', $person->unique_name),
+    'required' => 'required',
+    'data-slugify' => 'slug',
+]) ?>
+
+<?= form_label(
+    lang('Person.form.information_url'),
+    'information_url',
+    [],
+    lang('Person.form.information_url_hint'),
+    true
+) ?>
+<?= form_input([
+    'id' => 'information_url',
+    'name' => 'information_url',
+    'class' => 'form-input mb-4',
+    'value' => old('information_url', $person->information_url),
+]) ?>
+
+<?= form_label(lang('Person.form.image'), 'image') ?>
+<?= form_input([
+    'id' => 'image',
+    'name' => 'image',
+    'class' => 'form-input',
+    'type' => 'file',
+    'accept' => '.jpg,.jpeg,.png',
+]) ?>
+<small class="mb-4 text-gray-600"><?= lang(
+    'Person.form.image_size_hint'
+) ?></small>
+
+<?= form_section_close() ?>
+
+<?= button(
+    lang('Person.form.submit_edit'),
+    null,
+    ['variant' => 'primary'],
+    ['type' => 'submit', 'class' => 'self-end']
+) ?>
+
+
+<?= form_close() ?>
+
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/person/list.php b/app/Views/admin/person/list.php
new file mode 100644
index 0000000000000000000000000000000000000000..de4040fd7af17e371462ec4d3705fd331de7e0c3
--- /dev/null
+++ b/app/Views/admin/person/list.php
@@ -0,0 +1,65 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.all_persons') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Person.all_persons') ?> (<?= count($persons) ?>)
+<?= $this->endSection() ?>
+
+<?= $this->section('headerRight') ?>
+<?= button(
+    lang('Person.create'),
+    route_to('person-create'),
+    ['variant' => 'primary', 'iconLeft' => 'add'],
+    ['class' => 'mr-2']
+) ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<div class="flex flex-wrap">
+    <?php if (!empty($persons)): ?>
+        <?php foreach ($persons as $person): ?>
+            <article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
+            <img
+            alt="<?= $person->full_name ?>"
+            src="<?= $person->image
+                ->thumbnail_url ?>" class="object-cover w-40 w-full" />
+            <div class="p-2">
+                <a href="<?= route_to(
+                    'person-view',
+                    $person->id
+                ) ?>" class="hover:underline">
+                    <h2 class="font-semibold"><?= $person->full_name ?></h2>
+                </a>
+            </div>
+            <footer class="flex items-center justify-end p-2">
+                <a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
+                    'person-edit',
+                    $person->id
+                ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
+    'Person.edit'
+) ?>"><?= icon('edit') ?></a>
+                <a class="inline-flex p-2 mr-2 text-gray-700 bg-red-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
+                    'person-delete',
+                    $person->id
+                ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
+    'Person.delete'
+) ?>"><?= icon('delete-bin') ?></a>
+                <a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
+                    'person-view',
+                    $person->id
+                ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
+    'Person.view'
+) ?>"><?= icon('eye') ?></a>
+            </footer>
+        </article>
+        <?php endforeach; ?>
+    <?php else: ?>
+        <p class="italic"><?= lang('Person.no_person') ?></p>
+    <?php endif; ?>
+</div>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/person/view.php b/app/Views/admin/person/view.php
new file mode 100644
index 0000000000000000000000000000000000000000..99446eb41ea5b89a01e37bcf4b6016ebe08d39cc
--- /dev/null
+++ b/app/Views/admin/person/view.php
@@ -0,0 +1,38 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= $person->full_name ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= $person->full_name ?>
+
+<?= $this->endSection() ?>
+
+<?= $this->section('headerRight') ?>
+<?= button(
+    lang('Person.edit'),
+    route_to('person-edit', $person->id),
+    ['variant' => 'secondary', 'iconLeft' => 'edit'],
+    ['class' => 'mr-2']
+) ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<div class="flex flex-wrap">
+    <div class="w-full max-w-sm mb-6 md:mr-4">
+        <img
+            src="<?= $person->image->medium_url ?>"
+            alt="$person->full_name"
+            class="object-cover w-full rounded"
+        />
+    </div>
+
+    <section class="w-full prose">
+    <?= $person->full_name ?><br />
+    <a href="<?= $person->information_url ?>"><?= $person->information_url ?></a>
+    </section>
+</div>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php
index 7d525a25c4606d18b35ec6e0c987e74d4dd311d7..ffa8fe3c44370cd066448988ece8f2016eacc295 100644
--- a/app/Views/admin/podcast/_sidebar.php
+++ b/app/Views/admin/podcast/_sidebar.php
@@ -8,6 +8,10 @@ $podcastNavigation = [
         'icon' => 'mic',
         'items' => ['episode-list', 'episode-create'],
     ],
+    'persons' => [
+        'icon' => 'folder-user',
+        'items' => ['podcast-person-manage'],
+    ],
     'analytics' => [
         'icon' => 'line-chart',
         'items' => [
diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php
index db3049588538bf97abf18f9b1311cde879e6583a..7c6f17dba3a7bb0bc6c463b1c355d8f7a13afa62 100644
--- a/app/Views/admin/podcast/latest_episodes.php
+++ b/app/Views/admin/podcast/latest_episodes.php
@@ -58,6 +58,16 @@
                                     $podcast->id,
                                     $episode->id
                                 ) ?>"><?= lang('Episode.edit') ?></a>
+                                <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                    'episode-person-manage',
+                                    $podcast->id,
+                                    $episode->id
+                                ) ?>"><?= lang('Person.persons') ?></a>
+                                    <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                        'soundbites-edit',
+                                        $podcast->id,
+                                        $episode->id
+                                    ) ?>"><?= lang('Episode.soundbites') ?></a>
                                 <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
                                     'episode',
                                     $podcast->name,
diff --git a/app/Views/admin/podcast/person.php b/app/Views/admin/podcast/person.php
new file mode 100644
index 0000000000000000000000000000000000000000..1338f2ad1d24ef962a6713ebe1d08376eaafd85a
--- /dev/null
+++ b/app/Views/admin/podcast/person.php
@@ -0,0 +1,131 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.podcast_form.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Person.podcast_form.title') ?> (<?= count($podcastPersons) ?>)
+<?= $this->endSection() ?>
+
+<?= $this->section('headerRight') ?>
+<?= button(
+    lang('Person.create'),
+    route_to('person-create'),
+    ['variant' => 'primary', 'iconLeft' => 'add'],
+    ['class' => 'mr-2']
+) ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<?= form_open(route_to('podcast-person-edit', $podcast->id), [
+    'method' => 'post',
+    'class' => 'flex flex-col',
+]) ?>
+<?= csrf_field() ?>
+
+<?php if ($podcastPersons): ?>
+
+<?= form_section(
+    lang('Person.podcast_form.manage_section_title'),
+    lang('Person.podcast_form.manage_section_subtitle')
+) ?>
+
+
+<?= data_table(
+    [
+        [
+            'header' => lang('Person.podcast_form.person'),
+            'cell' => function ($podcastPerson) {
+                return '<div class="flex">' .
+                    '<a href="' .
+                    route_to('person-view', $podcastPerson->person->id) .
+                    "\"><img src=\"{$podcastPerson->person->image->thumbnail_url}\" alt=\"{$podcastPerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
+                    '<div class="flex flex-col ml-3">' .
+                    $podcastPerson->person->full_name .
+                    ($podcastPerson->person_group && $podcastPerson->person_role
+                        ? '<span class="text-sm text-gray-600">' .
+                            lang(
+                                "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label"
+                            ) .
+                            ' â–¸ ' .
+                            lang(
+                                "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label"
+                            ) .
+                            '</span>'
+                        : '') .
+                    (empty($podcastPerson->person->information_url)
+                        ? ''
+                        : "<a href=\"{$podcastPerson->person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
+                            $podcastPerson->person->information_url .
+                            '</a>') .
+                    '</div></div>';
+            },
+        ],
+        [
+            'header' => lang('Common.actions'),
+            'cell' => function ($podcastPerson) {
+                return button(
+                    lang('Person.podcast_form.remove'),
+                    route_to(
+                        'podcast-person-remove',
+                        $podcastPerson->podcast_id,
+                        $podcastPerson->id
+                    ),
+
+                    ['variant' => 'danger', 'size' => 'small']
+                );
+            },
+        ],
+    ],
+    $podcastPersons
+) ?>
+
+<?= form_section_close() ?>
+<?php endif; ?>
+
+
+<?= form_section(
+    lang('Person.podcast_form.add_section_title'),
+    lang('Person.podcast_form.add_section_subtitle')
+) ?>
+
+<?= form_label(
+    lang('Person.podcast_form.person'),
+    'person',
+    [],
+    lang('Person.podcast_form.person_hint')
+) ?>
+<?= form_multiselect('person[]', $personOptions, old('person', []), [
+    'id' => 'person',
+    'class' => 'form-select mb-4',
+    'required' => 'required',
+]) ?>
+
+<?= form_label(
+    lang('Person.podcast_form.group_role'),
+    'group_role',
+    [],
+
+    lang('Person.podcast_form.group_role_hint'),
+    true
+) ?>
+<?= form_multiselect(
+    'person_group_role[]',
+    $taxonomyOptions,
+    old('person_group_role', []),
+    ['id' => 'person_group_role', 'class' => 'form-select mb-4']
+) ?>
+        
+    
+<?= form_section_close() ?>
+<?= button(
+    lang('Person.podcast_form.submit_add'),
+    null,
+    ['variant' => 'primary'],
+    ['type' => 'submit', 'class' => 'self-end']
+) ?> 
+<?= form_close() ?>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/credits.php b/app/Views/credits.php
new file mode 100644
index 0000000000000000000000000000000000000000..97152c0be232e6249c51b111b9aa2a59c0176302
--- /dev/null
+++ b/app/Views/credits.php
@@ -0,0 +1,49 @@
+<?= $this->extend('_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Person.credits') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<div class="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
+<?php foreach ($credits as $groupSlug => $groups): ?>
+    <?php if (
+        $groupSlug
+    ): ?><div class="col-span-1 mt-12 mb-2 text-xl font-bold text-gray-500 md:text-2xl md:col-span-2 "><?= $groups[
+    'group_label'
+] ?></div><?php endif; ?>
+    <?php foreach ($groups['persons'] as $personId => $persons): ?>
+        <div class="flex mt-2 mb-2">
+            <img src="<?= $persons['thumbnail_url'] ?>" alt="<?= $persons[
+    'full_name'
+] ?>" class="object-cover w-16 h-16 border-4 rounded-full md:h-24 md:w-24 border-gray" />
+            <div class="flex flex-col ml-3 mr-4"><span class="text-lg font-bold text-gray-700 md:text-xl"><?= $persons[
+                'full_name'
+            ] ?></span>
+            <?php if (
+                !empty($persons['information_url'])
+            ): ?><a href="<?= $persons[
+    'information_url'
+] ?>" class="text-sm text-blue-800 hover:underline" target="_blank" rel="noreferrer noopener"><?= $persons[
+    'information_url'
+] ?></a><?php endif; ?></div>
+        </div>
+        <div class="flex flex-col">
+        <?php foreach ($persons['roles'] as $role_slug => $role_array): ?>
+            <?= $role_array['role_label'] ?>
+            
+            <?php foreach ($role_array['is_in'] as $isIn): ?>
+               <a href="<?= $isIn[
+                   'link'
+               ] ?>" class="text-sm text-gray-500 hover:underline"><?= $isIn[
+    'title'
+] ?></a>
+            <?php endforeach; ?>
+            
+        <?php endforeach; ?>
+        </div>
+    <?php endforeach; ?>
+<?php endforeach; ?>
+</div>
+<?php $this->endSection(); ?>
diff --git a/app/Views/episode.php b/app/Views/episode.php
index 6bdf4840b82185c712798e3f48e01c23cf744c12..0b116d7bd4bf97c8bd4dbd89dd8e96af283668da 100644
--- a/app/Views/episode.php
+++ b/app/Views/episode.php
@@ -100,11 +100,28 @@
                 <?= format_duration($episode->enclosure_duration) ?>
               </time>
           </div>
+          <div class="flex mt-2 mb-1 space-x-2">
+            <?php foreach ($persons as $person): ?>
+                <?php if (!empty($person['information_url'])): ?>
+                    <a href="<?= $person[
+                        'information_url'
+                    ] ?>" target="_blank" rel="noreferrer noopener">
+                <?php endif; ?>
+                <img src="<?= $person['thumbnail_url'] ?>" alt="<?= $person[
+    'full_name'
+] ?>" title="[<?= $person['full_name'] ?>] <?= $person[
+    'roles'
+] ?>" class="object-cover w-12 h-12 rounded-full" />
+                <?php if (!empty($person['information_url'])): ?>
+                    </a>
+                <?php endif; ?>
+            <?php endforeach; ?>
+          </div>
           <?= location_link(
               $episode->location_name,
               $episode->location_geo,
               $episode->location_osmid,
-              'self-start mt-2'
+              'self-start mt-2 mb-2'
           ) ?>
           <audio controls preload="none" class="w-full mt-auto">
             <source src="<?= $episode->enclosure_web_url ?>" type="<?= $episode->enclosure_type ?>">
diff --git a/app/Views/podcast.php b/app/Views/podcast.php
index eaafbd9fb52244b13e09a67de9b9dbcf696f24ee..29fed4b48ed4d97e1dd8547bab98ebd8c313c83c 100644
--- a/app/Views/podcast.php
+++ b/app/Views/podcast.php
@@ -114,6 +114,26 @@
                         <?php endif; ?>
                     <?php endforeach; ?>
                     </div>
+
+                    <div class="flex mb-2 space-x-2">
+                    <?php foreach ($personArray as $person): ?>
+                        <?php if (!empty($person['information_url'])): ?>
+                            <a href="<?= $person[
+                                'information_url'
+                            ] ?>" target="_blank" rel="noreferrer noopener">
+                        <?php endif; ?>
+                        <img src="<?= $person[
+                            'thumbnail_url'
+                        ] ?>" alt="<?= $person[
+    'full_name'
+] ?>" title="[<?= $person['full_name'] ?>] <?= $person[
+    'roles'
+] ?>" class="object-cover w-12 h-12 rounded-full" />
+                        <?php if (!empty($person['information_url'])): ?>
+                            </a>
+                        <?php endif; ?>
+                    <?php endforeach; ?>
+                    </div>
                     
                     <div class="mb-2 opacity-75">
                         <?= $podcast->description_html ?>
diff --git a/composer.json b/composer.json
index 757d0d948ce0f8879bb9cc2f04d63ac8f8efbecf..3f6e47a037b10dab6059c2310dc19851b0833ac0 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,8 @@
     "vlucas/phpdotenv": "^5.2",
     "league/html-to-markdown": "^4.10",
     "opawg/user-agents-php": "^1.0",
-    "podlibre/ipcat": "^1.0"
+    "podlibre/ipcat": "^1.0",
+    "podlibre/podcast-namespace": "^1.0.6"
   },
   "require-dev": {
     "mikey179/vfsstream": "1.6.*",
@@ -33,13 +34,19 @@
     "post-install-cmd": [
       "@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php >  vendor/opawg/user-agents-php/src/UserAgents.php",
       "@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php >  vendor/opawg/user-agents-php/src/UserAgentsRSS.php",
-      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php"
+      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php",
+      "@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json >  app/Language/en/PersonsTaxonomy.php",
+      "@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-fr.json >  app/Language/fr/PersonsTaxonomy.php",
+      "@php vendor/podlibre/podcast-namespace/src/ReversedTaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json >  vendor/podlibre/podcast-namespace/src/ReversedTaxonomy.php"
     ],
     "post-update-cmd": [
       "@composer dump-autoload",
       "@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php >  vendor/opawg/user-agents-php/src/UserAgents.php",
       "@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php >  vendor/opawg/user-agents-php/src/UserAgentsRSS.php",
-      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php"
+      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php",
+      "@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json >  app/Language/en/PersonsTaxonomy.php",
+      "@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-fr.json >  app/Language/fr/PersonsTaxonomy.php",
+      "@php vendor/podlibre/podcast-namespace/src/ReversedTaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json >  vendor/podlibre/podcast-namespace/src/ReversedTaxonomy.php"
     ]
   },
   "support": {
diff --git a/composer.lock b/composer.lock
index d8c81dfc4f69a97b9284be4cc0e01ce947462386..1b9ff8f66f222bd7afed3da7aa130bca510a828f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1071,6 +1071,35 @@
             },
             "time": "2020-10-05T17:15:07+00:00"
         },
+        {
+            "name": "podlibre/podcast-namespace",
+            "version": "v1.0.6",
+            "source": {
+                "type": "git",
+                "url": "https://code.podlibre.org/podlibre/podcastnamespace",
+                "reference": "4525c06ee9dd95bb745ee875d55b64a053c74cd6"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Podlibre\\PodcastNamespace\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Bellamy",
+                    "email": "ben@podlibre.org",
+                    "homepage": "https://podlibre.org/"
+                }
+            ],
+            "description": "PHP implementation for the Podcast Namespace.",
+            "homepage": "https://code.podlibre.org/podlibre/podcastnamespace",
+            "time": "2021-01-14T15:47:06+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "1.0.1",