diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 3673a17b9c329929adbb04d139e21c56c375fec8..560346cff51de9746cd4ad8ff7f3ca0d9dbbef5c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -30,7 +30,13 @@
           "spark": "php",
           "env": "dotenv",
           ".rsync-filter": "diff"
-        }
+        },
+        "json.schemas": [
+          {
+            "fileMatch": ["plugins/**/manifest.json"],
+            "url": "/workspaces/castopod/modules/Plugins/manifest.schema.json"
+          }
+        ]
       },
       "extensions": [
         "astro-build.astro-vscode",
diff --git a/modules/Plugins/BasePlugin.php b/modules/Plugins/BasePlugin.php
index 3e98dc98e6a33b767362025dd641eeed5a78519c..cf3fd0fa67cac20691eea081a66d04474e175fe8 100644
--- a/modules/Plugins/BasePlugin.php
+++ b/modules/Plugins/BasePlugin.php
@@ -53,7 +53,7 @@ abstract class BasePlugin implements PluginInterface
      */
     public function __set(string $name, array|string $value): void
     {
-        $this->{$name} = $name === 'releaseDate' ? Time::createFromFormat('Y-m-d', $value) : $value;
+        $this->{$name} = $value;
     }
 
     public function init(): void
@@ -161,14 +161,17 @@ abstract class BasePlugin implements PluginInterface
 
         if (array_key_exists('settings', $manifest)) {
             $fieldRules = [
-                'key'         => 'required|alpha_numeric',
-                'name'        => 'required|max_length[32]',
-                'description' => 'permit_empty|max_length[128]',
+                'key'    => 'required|alpha_numeric',
+                'label'  => 'required|max_length[32]',
+                'hint'   => 'permit_empty|max_length[128]',
+                'helper' => 'permit_empty|max_length[128]',
             ];
             $defaultField = [
-                'key'         => '',
-                'name'        => '',
-                'description' => '',
+                'key'      => '',
+                'label'    => '',
+                'hint'     => '',
+                'helper'   => '',
+                'optional' => false,
             ];
             $validation->setRules($fieldRules);
             foreach ($manifest['settings'] as $key => $settings) {
@@ -185,14 +188,12 @@ abstract class BasePlugin implements PluginInterface
         $rules = [
             'name'         => 'required|max_length[32]',
             'version'      => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]',
-            'compatible'   => 'required|in_list[1.0]',
             'description'  => 'max_length[128]',
-            'releaseDate'  => 'valid_date[Y-m-d]',
             'license'      => 'in_list[MIT]',
             'author.name'  => 'permit_empty|max_length[32]',
             'author.email' => 'permit_empty|valid_email',
             'author.url'   => 'permit_empty|valid_url_strict',
-            'website'      => 'valid_url_strict',
+            'homepage'     => 'valid_url_strict',
             'keywords.*'   => 'permit_empty|in_list[seo,podcasting20,analytics]',
             'hooks.*'      => 'permit_empty|in_list[' . implode(',', Plugins::HOOKS) . ']',
             'settings'     => 'permit_empty',
@@ -206,10 +207,9 @@ abstract class BasePlugin implements PluginInterface
 
         $defaultAttributes = [
             'description' => '',
-            'releaseDate' => '',
             'license'     => '',
             'author'      => [],
-            'website'     => '',
+            'homepage'    => '',
             'hooks'       => [],
             'keywords'    => [],
             'settings'    => [
diff --git a/modules/Plugins/manifest.schema.json b/modules/Plugins/manifest.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..58528a1f1ad93db6ec2e3f18811e778af136d709
--- /dev/null
+++ b/modules/Plugins/manifest.schema.json
@@ -0,0 +1,191 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schemas/manifest.json",
+  "title": "JSON schema for Castopod Plugins's manifest.json files",
+  "description": "The Castopod plugin manifest defines both metadata and behavior of a plugin",
+  "type": "object",
+  "properties": {
+    "name": {
+      "description": "The plugin name, including 'vendor-name/' prefix",
+      "type": "string",
+      "pattern": "^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*$",
+      "examples": ["acme/hello-world"]
+    },
+    "version": {
+      "description": "The plugin's semantic version. See https://semver.org/",
+      "type": "string",
+      "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
+      "examples": ["1.0.0"]
+    },
+    "description": {
+      "description": "This helps people discover your plugin as it's listed in repositories",
+      "type": "string"
+    },
+    "author": {
+      "$ref": "#/$defs/person"
+    },
+    "authors": {
+      "type": "array",
+      "items": {
+        "$ref": "#/$defs/person"
+      }
+    },
+    "homepage": {
+      "description": "The URL to the plugin homepage",
+      "type": "string",
+      "format": "uri"
+    },
+    "license": {
+      "description": "You should specify a license for your plugin so that people know how they are permitted to use it, and any restrictions you're placing on it.",
+      "default": "UNLICENSED",
+      "anyOf": [
+        {
+          "type": "string"
+        },
+        {
+          "enum": [
+            "AGPL-3.0-only",
+            "AGPL-3.0-or-later",
+            "Apache-2.0",
+            "BSL-1.0",
+            "GPL-3.0-only",
+            "GPL-3.0-or-later",
+            "LGPL-3.0-only",
+            "LGPL-3.0-or-later",
+            "MIT",
+            "MPL-2.0",
+            "Unlicense",
+            "UNLICENSED"
+          ]
+        }
+      ]
+    },
+    "private": {
+      "type": "boolean",
+      "description": "If set to true, then repositories should refuse to publish it."
+    },
+    "keywords": {
+      "description": "This helps people discover your plugin as it's listed in repositories",
+      "type": "array",
+      "items": {
+        "anyOf": [
+          {
+            "type": "string"
+          },
+          {
+            "enum": [
+              "accessibility",
+              "analytics",
+              "monetization",
+              "podcasting2",
+              "privacy",
+              "seo"
+            ]
+          }
+        ]
+      },
+      "uniqueItems": true
+    },
+    "hooks": {
+      "description": "The hooks used by the plugin.",
+      "type": "array",
+      "items": {
+        "enum": ["channelTag", "itemTag", "siteHead"]
+      },
+      "uniqueItems": true
+    },
+    "settings": {
+      "type": "object",
+      "properties": {
+        "general": {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/settings-field"
+          }
+        },
+        "podcast": {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/settings-field"
+          }
+        },
+        "episode": {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/settings-field"
+          }
+        }
+      }
+    },
+    "files": {
+      "description": "List of files to include in your plugin package. If you include a folder in the array, all files inside it will also be included.",
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "repository": {
+      "description": "Specify the place where your plugin code lives. This is helpful for people who want to contribute.",
+      "type": ["object", "string"],
+      "properties": {
+        "type": {
+          "type": "string"
+        },
+        "url": {
+          "type": "string"
+        },
+        "directory": {
+          "type": "string"
+        }
+      }
+    }
+  },
+  "required": ["name", "version"],
+  "additionalProperties": false,
+  "$defs": {
+    "person": {
+      "description": "A person who has been involved in creating or maintaining this plugin.",
+      "type": ["object", "string"],
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "email": {
+          "type": "string",
+          "format": "email"
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
+      }
+    },
+    "settings-field": {
+      "type": "object",
+      "properties": {
+        "type": {
+          "enum": ["text", "email", "url", "markdown", "number", "switch"],
+          "default": "text"
+        },
+        "key": {
+          "type": "string"
+        },
+        "label": {
+          "type": "string"
+        },
+        "hint": {
+          "type": "string"
+        },
+        "helper": {
+          "type": "string"
+        },
+        "optional": {
+          "type": "boolean"
+        }
+      },
+      "required": ["key", "label"],
+      "additionalProperties": false
+    }
+  }
+}
diff --git a/themes/cp_admin/plugins/_plugin.php b/themes/cp_admin/plugins/_plugin.php
index 9cb7e0882c438b35c4cf6aafae49e34be879e47b..02441de500b5da5f03ee44344e2287a2793b15ad 100644
--- a/themes/cp_admin/plugins/_plugin.php
+++ b/themes/cp_admin/plugins/_plugin.php
@@ -12,7 +12,7 @@
         <p class="mt-2 text-gray-600"><?= $plugin->getDescription() ?></p>
     </div>
     <footer class="flex items-center justify-between mt-4">
-        <a href="<?= $plugin->website ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
+        <a href="<?= $plugin->homepage ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
             'class' => 'text-gray-500',
         ]) . lang('Plugins.website') ?></a>
         <div class="flex gap-x-2">
diff --git a/themes/cp_admin/plugins/_settings.php b/themes/cp_admin/plugins/_settings.php
index 7bc947acbe4bd6105ec63a76f2421f6b38917a33..d58644c6f7e51cbcac211930327092dd705d509b 100644
--- a/themes/cp_admin/plugins/_settings.php
+++ b/themes/cp_admin/plugins/_settings.php
@@ -2,9 +2,11 @@
 <?= csrf_field() ?>
 <?php foreach ($plugin->settings[$type] as $field): ?>
 <Forms.Field
-    name="<?= $field['key'] ?>"
-    label="<?= $field['name'] ?>"
-    hint="<?= $field['description'] ?>"
+    name="<?= esc($field['key']) ?>"
+    label="<?= esc($field['label']) ?>"
+    hint="<?= esc($field['hint']) ?>"
+    helper="<?= esc($field['helper']) ?>"
+    required="<?= $field['optional'] === 'true' ? 'false' : 'true' ?>"
     value="<?= get_plugin_option($plugin->getKey(), $field['key'], $context) ?>"
 />
 <?php endforeach; ?>