diff --git a/core/config.php b/core/config.php
index c88c0dbae..4d130c577 100644
--- a/core/config.php
+++ b/core/config.php
@@ -323,4 +323,17 @@ protected function save(string $name): void
 
 abstract class ConfigGroup
 {
+    public static function get_group_for_entry_by_name(string $name): ?ConfigGroup
+    {
+        foreach (get_subclasses_of(ConfigGroup::class) as $class) {
+            $config = new $class();
+            assert(is_a($config, ConfigGroup::class));
+            foreach ((new \ReflectionClass($class))->getConstants() as $const => $value) {
+                if ($value === $name) {
+                    return $config;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/core/tests/ConfigTest.php b/core/tests/ConfigTest.php
new file mode 100644
index 000000000..d16394126
--- /dev/null
+++ b/core/tests/ConfigTest.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shimmie2;
+
+require_once "core/imageboard/image.php";
+
+class ConfigTest extends ShimmiePHPUnitTestCase
+{
+    public function testConfigGroup(): void
+    {
+        $conf = ConfigGroup::get_group_for_entry_by_name("comment_limit");
+        $this->assertNotNull($conf);
+        $this->assertEquals(CommentConfig::class, $conf::class);
+    }
+}
diff --git a/ext/blotter/config.php b/ext/blotter/config.php
new file mode 100644
index 000000000..47179e437
--- /dev/null
+++ b/ext/blotter/config.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shimmie2;
+
+class BlotterConfig extends ConfigGroup
+{
+    public const VERSION = "blotter_version";
+    public const COLOR = "blotter_color";
+    public const POSITION = "blotter_position";
+    public const RECENT = "blotter_recent";
+}
diff --git a/ext/blotter/main.php b/ext/blotter/main.php
index 87439c373..362ee2d3d 100644
--- a/ext/blotter/main.php
+++ b/ext/blotter/main.php
@@ -12,16 +12,16 @@ class Blotter extends Extension
     public function onInitExt(InitExtEvent $event): void
     {
         global $config;
-        $config->set_default_int("blotter_recent", 5);
-        $config->set_default_string("blotter_color", "FF0000");
-        $config->set_default_string("blotter_position", "subheading");
+        $config->set_default_int(BlotterConfig::RECENT, 5);
+        $config->set_default_string(BlotterConfig::COLOR, "FF0000");
+        $config->set_default_string(BlotterConfig::POSITION, "subheading");
     }
 
     public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
     {
         global $database;
 
-        if ($this->get_version("blotter_version") < 1) {
+        if ($this->get_version(BlotterConfig::VERSION) < 1) {
             $database->create_table("blotter", "
                 id SCORE_AIPK,
                 entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -34,11 +34,11 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
                 ["text" => "Installed the blotter extension!", "important" => true]
             );
             log_info("blotter", "Installed tables for blotter extension.");
-            $this->set_version("blotter_version", 2);
+            $this->set_version(BlotterConfig::VERSION, 2);
         }
-        if ($this->get_version("blotter_version") < 2) {
+        if ($this->get_version(BlotterConfig::VERSION) < 2) {
             $database->standardise_boolean("blotter", "important");
-            $this->set_version("blotter_version", 2);
+            $this->set_version(BlotterConfig::VERSION, 2);
         }
     }
 
@@ -110,7 +110,7 @@ private function display_blotter(): void
         global $database, $config;
         $entries = $database->get_all(
             'SELECT * FROM blotter ORDER BY id DESC LIMIT :limit',
-            ["limit" => $config->get_int("blotter_recent", 5)]
+            ["limit" => $config->get_int(BlotterConfig::RECENT, 5)]
         );
         $this->theme->display_blotter($entries);
     }
diff --git a/ext/blotter/theme.php b/ext/blotter/theme.php
index 6ef228794..8bd62caa2 100644
--- a/ext/blotter/theme.php
+++ b/ext/blotter/theme.php
@@ -41,7 +41,7 @@ public function display_blotter(array $entries): void
     {
         global $page, $config;
         $html = $this->get_html_for_blotter($entries);
-        $position = $config->get_string("blotter_position", "subheading");
+        $position = $config->get_string(BlotterConfig::POSITION, "subheading");
         $page->add_block(new Block(null, rawHTML($html), $position, 20));
     }
 
@@ -127,7 +127,7 @@ private function get_html_for_blotter_page(array $entries): string
          * This one displays a list of all blotter entries.
          */
         global $config;
-        $i_color = $config->get_string("blotter_color", "#FF0000");
+        $i_color = $config->get_string(BlotterConfig::COLOR, "#FF0000");
         $html = "<pre>";
 
         $num_entries = count($entries);
@@ -158,8 +158,8 @@ private function get_html_for_blotter_page(array $entries): string
     private function get_html_for_blotter(array $entries): string
     {
         global $config;
-        $i_color = $config->get_string("blotter_color", "#FF0000");
-        $position = $config->get_string("blotter_position", "subheading");
+        $i_color = $config->get_string(BlotterConfig::COLOR, "#FF0000");
+        $position = $config->get_string(BlotterConfig::POSITION, "subheading");
         $entries_list = "";
         foreach ($entries as $entry) {
             /**
diff --git a/ext/comment/config.php b/ext/comment/config.php
new file mode 100644
index 000000000..e9e6525d5
--- /dev/null
+++ b/ext/comment/config.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shimmie2;
+
+class CommentConfig extends ConfigGroup
+{
+    public const VERSION = "ext_comments_version";
+    public const COUNT = "comment_count";
+    public const WINDOW = "comment_window";
+    public const LIMIT = "comment_limit";
+    public const LIST_COUNT = "comment_list_count";
+    public const CAPTCHA = "comment_captcha";
+    public const WORDPRESS_KEY = "comment_wordpress_key";
+    public const SHOW_REPEAT_ANONS = "comment_samefags_public";
+}
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 98e3da8e3..d550ce80f 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -6,7 +6,6 @@
 
 use GQLA\Type;
 use GQLA\Field;
-use GQLA\Query;
 use GQLA\Mutation;
 
 require_once "vendor/ifixit/php-akismet/akismet.class.php";
@@ -125,19 +124,19 @@ class CommentList extends Extension
     public function onInitExt(InitExtEvent $event): void
     {
         global $config;
-        $config->set_default_int('comment_window', 5);
-        $config->set_default_int('comment_limit', 10);
-        $config->set_default_int('comment_list_count', 10);
-        $config->set_default_int('comment_count', 5);
-        $config->set_default_bool('comment_captcha', false);
+        $config->set_default_int(CommentConfig::WINDOW, 5);
+        $config->set_default_int(CommentConfig::LIMIT, 10);
+        $config->set_default_int(CommentConfig::LIST_COUNT, 10);
+        $config->set_default_int(CommentConfig::COUNT, 5);
+        $config->set_default_bool(CommentConfig::CAPTCHA, false);
     }
 
     public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
     {
         global $database;
-        if ($this->get_version("ext_comments_version") < 3) {
+        if ($this->get_version(CommentConfig::VERSION) < 3) {
             // shortcut to latest
-            if ($this->get_version("ext_comments_version") < 1) {
+            if ($this->get_version(CommentConfig::VERSION) < 1) {
                 $database->create_table("comments", "
 					id SCORE_AIPK,
 					image_id INTEGER NOT NULL,
@@ -151,11 +150,11 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
                 $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", []);
                 $database->execute("CREATE INDEX comments_owner_id_idx ON comments(owner_id)", []);
                 $database->execute("CREATE INDEX comments_posted_idx ON comments(posted)", []);
-                $this->set_version("ext_comments_version", 3);
+                $this->set_version(CommentConfig::VERSION, 3);
             }
 
             // the whole history
-            if ($this->get_version("ext_comments_version") < 1) {
+            if ($this->get_version(CommentConfig::VERSION) < 1) {
                 $database->create_table("comments", "
 					id SCORE_AIPK,
 					image_id INTEGER NOT NULL,
@@ -165,17 +164,17 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
 					comment TEXT NOT NULL
 				");
                 $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", []);
-                $this->set_version("ext_comments_version", 1);
+                $this->set_version(CommentConfig::VERSION, 1);
             }
 
-            if ($this->get_version("ext_comments_version") == 1) {
+            if ($this->get_version(CommentConfig::VERSION) == 1) {
                 $database->execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)");
                 $database->execute("CREATE INDEX comments_posted ON comments(posted)");
-                $this->set_version("ext_comments_version", 2);
+                $this->set_version(CommentConfig::VERSION, 2);
             }
 
-            if ($this->get_version("ext_comments_version") == 2) {
-                $this->set_version("ext_comments_version", 3);
+            if ($this->get_version(CommentConfig::VERSION) == 2) {
+                $this->set_version(CommentConfig::VERSION, 3);
                 $database->execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE");
                 $database->execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT");
             }
@@ -310,7 +309,7 @@ public function onAdminBuilding(AdminBuildingEvent $event): void
     public function onPostListBuilding(PostListBuildingEvent $event): void
     {
         global $cache, $config;
-        $cc = $config->get_int("comment_count");
+        $cc = $config->get_int(CommentConfig::COUNT);
         if ($cc > 0) {
             $recent = cache_get_or_set("recent_comments", fn () => $this->get_recent_comments($cc), 60);
             if (count($recent) > 0) {
@@ -477,8 +476,8 @@ private function is_comment_limit_hit(): bool
             return false;
         }
 
-        $window = $config->get_int('comment_window');
-        $max = $config->get_int('comment_limit');
+        $window = $config->get_int(CommentConfig::WINDOW);
+        $max = $config->get_int(CommentConfig::LIMIT);
 
         if ($database->get_driver_id() == DatabaseDriverID::MYSQL) {
             $window_sql = "interval $window minute";
@@ -511,7 +510,7 @@ public static function get_hash(): string
     private function is_spam_akismet(string $text): bool
     {
         global $config, $user;
-        $key = $config->get_string('comment_wordpress_key');
+        $key = $config->get_string(CommentConfig::WORDPRESS_KEY);
         if (!is_null($key) && strlen($key) > 0) {
             $comment = [
                 'author'       => $user->name,
@@ -608,7 +607,7 @@ private function comment_checks(int $image_id, User $user, string $comment): voi
         }
 
         // rate-limited external service checks last
-        elseif ($config->get_bool('comment_captcha') && !captcha_check()) {
+        elseif ($config->get_bool(CommentConfig::CAPTCHA) && !captcha_check()) {
             throw new CommentPostingException("Error in captcha");
         } elseif ($user->is_anonymous() && $this->is_spam_akismet($comment)) {
             throw new CommentPostingException("Akismet thinks that your comment is spam. Try rewriting the comment, or logging in.");
diff --git a/ext/comment/test.php b/ext/comment/test.php
index 9d7e9fdbd..5d30675b9 100644
--- a/ext/comment/test.php
+++ b/ext/comment/test.php
@@ -10,14 +10,14 @@ public function setUp(): void
     {
         global $config;
         parent::setUp();
-        $config->set_int("comment_limit", 100);
+        $config->set_int(CommentConfig::LIMIT, 100);
         $this->log_out();
     }
 
     public function tearDown(): void
     {
         global $config;
-        $config->set_int("comment_limit", 10);
+        $config->set_int(CommentConfig::LIMIT, 10);
         parent::tearDown();
     }
 
diff --git a/ext/comment/theme.php b/ext/comment/theme.php
index 6c42b4b3d..d4677cdc8 100644
--- a/ext/comment/theme.php
+++ b/ext/comment/theme.php
@@ -41,8 +41,8 @@ public function display_comment_list(array $images, int $page_number, int $total
         // parts for each image
         $position = 10;
 
-        $comment_limit = $config->get_int("comment_list_count", 10);
-        $comment_captcha = $config->get_bool('comment_captcha');
+        $comment_limit = $config->get_int(CommentConfig::LIST_COUNT, 10);
+        $comment_captcha = $config->get_bool(CommentConfig::CAPTCHA);
 
         foreach ($images as $pair) {
             $image = $pair[0];
@@ -222,7 +222,7 @@ protected function comment_to_html(Comment $comment, bool $trim = false): string
                 }
                 #if($user->can(UserAbilities::VIEW_IP)) {
                 #$style = " style='color: ".$this->get_anon_colour($comment->poster_ip).";'";
-                if ($user->can(Permissions::VIEW_IP) || $config->get_bool("comment_samefags_public", false)) {
+                if ($user->can(Permissions::VIEW_IP) || $config->get_bool(CommentConfig::SHOW_REPEAT_ANONS, false)) {
                     if ($this->anon_map[$comment->poster_ip] != $this->anon_id) {
                         $anoncode2 = '<sup>('.$this->anon_map[$comment->poster_ip].')</sup>';
                     }
diff --git a/ext/setup/style.css b/ext/setup/style.css
index a20a2ea8f..63a0d123d 100644
--- a/ext/setup/style.css
+++ b/ext/setup/style.css
@@ -16,6 +16,10 @@
 	resize: vertical;
 }
 
+.advanced_settings INPUT {
+	width: 100%;
+}
+
 #Setupmain {
 	box-shadow: none;
 }
@@ -35,4 +39,4 @@
 	margin-top: 1em;
 	padding: 1em;
 	width: 100%;
-}
\ No newline at end of file
+}
diff --git a/ext/setup/theme.php b/ext/setup/theme.php
index c7d26ee19..fb8fe79d9 100644
--- a/ext/setup/theme.php
+++ b/ext/setup/theme.php
@@ -6,6 +6,15 @@
 
 use MicroHTML\HTMLElement;
 
+use function MicroHTML\INPUT;
+use function MicroHTML\TABLE;
+use function MicroHTML\TBODY;
+use function MicroHTML\TD;
+use function MicroHTML\TEXTAREA;
+use function MicroHTML\TFOOT;
+use function MicroHTML\TH;
+use function MicroHTML\THEAD;
+use function MicroHTML\TR;
 use function MicroHTML\rawHTML;
 
 class SetupTheme extends Themelet
@@ -51,39 +60,57 @@ public function display_page(Page $page, SetupPanel $panel): void
      */
     public function display_advanced(Page $page, array $options): void
     {
-        $h_rows = "";
+        $rows = TBODY();
         ksort($options);
         foreach ($options as $name => $value) {
+            $ext = ConfigGroup::get_group_for_entry_by_name($name);
+            if ($ext) {
+                $ext_name = \Safe\preg_replace("#Shimmie2.(.*)Config#", '$1', $ext::class);
+            } else {
+                $ext_name = "";
+            }
+
             if (is_null($value)) {
                 $value = '';
             }
 
-            $h_name = html_escape($name);
-            $h_value = html_escape((string)$value);
-
-            $h_box = "";
+            $valbox = TD();
             if (is_string($value) && str_contains($value, "\n")) {
-                $h_box .= "<textarea cols='50' rows='4' name='_config_$h_name'>$h_value</textarea>";
+                $valbox->appendChild(TEXTAREA(
+                    ['name' => "_config_$name", 'cols' => 50, 'rows' => 4],
+                    $value,
+                ));
             } else {
-                $h_box .= "<input type='text' name='_config_$h_name' value='$h_value'>";
+                $valbox->appendChild(INPUT(
+                    ['type' => 'text', 'name' => "_config_$name", 'value' => $value],
+                ));
             }
-            $h_box .= "<input type='hidden' name='_type_$h_name' value='string'>";
-            $h_rows .= "<tr><td>$h_name</td><td>$h_box</td></tr>";
+            $valbox->appendChild(INPUT(
+                ['type' => 'hidden', 'name' => '_type_' . $name, 'value' => 'string'],
+            ));
+
+            $rows->appendChild(TR(TD($ext_name), TD($name), $valbox));
         }
 
-        $table = "
-			".make_form(make_link("setup/save"))."
-				<table id='settings' class='zebra'>
-					<thead><tr><th width='25%'>Name</th><th>Value</th></tr></thead>
-					<tbody>$h_rows</tbody>
-					<tfoot><tr><td colspan='2'><input type='submit' value='Save Settings'></td></tr></tfoot>
-				</table>
-			</form>
-			";
+        $table = SHM_SIMPLE_FORM(
+            "setup/save",
+            TABLE(
+                ['id' => 'settings', 'class' => 'zebra advanced_settings'],
+                THEAD(TR(
+                    TH(['width' => '20%'], 'Group'),
+                    TH(['width' => '20%'], 'Name'),
+                    TH('Value'),
+                )),
+                $rows,
+                TFOOT(TR(
+                    TD(["colspan" => 3], INPUT(['type' => 'submit', 'value' => 'Save Settings']))
+                )),
+            )
+        );
 
         $page->set_title("Shimmie Setup");
         $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0));
-        $page->add_block(new Block("Setup", rawHTML($table)));
+        $page->add_block(new Block("Setup", $table));
     }
 
     protected function build_navigation(): HTMLElement
diff --git a/ext/user/main.php b/ext/user/main.php
index 78c1504c3..91b55ad2c 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -99,7 +99,7 @@ public static function login(string $username, string $password): LoginResult
             );
         } catch (UserNotFound $ex) {
             return new LoginResult(
-                User::by_id($config->get_int("anon_id", 0)),
+                User::by_id($config->get_int(UserPageConfig::ANON_ID, 0)),
                 null,
                 "No user found"
             );
@@ -119,7 +119,7 @@ public static function create_user(string $username, string $password1, string $
             );
         } catch (UserCreationException $ex) {
             return new LoginResult(
-                User::by_id($config->get_int("anon_id", 0)),
+                User::by_id($config->get_int(UserPageConfig::ANON_ID, 0)),
                 null,
                 $ex->getMessage()
             );
@@ -135,14 +135,14 @@ class UserPage extends Extension
     public function onInitExt(InitExtEvent $event): void
     {
         global $config;
-        $config->set_default_bool("login_signup_enabled", true);
-        $config->set_default_int("login_memory", 365);
-        $config->set_default_string("avatar_host", "none");
-        $config->set_default_int("avatar_gravatar_size", 80);
-        $config->set_default_string("avatar_gravatar_default", "");
-        $config->set_default_string("avatar_gravatar_rating", "g");
-        $config->set_default_bool("login_tac_bbcode", true);
-        $config->set_default_bool("user_email_required", false);
+        $config->set_default_bool(UserPageConfig::SIGNUP_ENABLED, true);
+        $config->set_default_int(UserPageConfig::LOGIN_MEMORY, 365);
+        $config->set_default_string(AvatarConfig::HOST, "none");
+        $config->set_default_int(AvatarConfig::GRAVATAR_SIZE, 80);
+        $config->set_default_string(AvatarConfig::GRAVATAR_DEFAULT, "");
+        $config->set_default_string(AvatarConfig::GRAVATAR_RATING, "g");
+        $config->set_default_bool(UserPageConfig::LOGIN_TAC_BBCODE, true);
+        $config->set_default_bool(UserPageConfig::USER_EMAIL_REQUIRED, false);
     }
 
     public function onUserLogin(UserLoginEvent $event): void
@@ -174,7 +174,7 @@ public function onPageRequest(PageRequestEvent $event): void
         }
         if ($event->page_matches("user_admin/create", method: "GET", permission: Permissions::CREATE_USER)) {
             global $config, $page, $user;
-            if (!$config->get_bool("login_signup_enabled")) {
+            if (!$config->get_bool(UserPageConfig::SIGNUP_ENABLED)) {
                 $this->theme->display_signups_disabled($page);
                 return;
             }
@@ -182,7 +182,7 @@ public function onPageRequest(PageRequestEvent $event): void
         }
         if ($event->page_matches("user_admin/create", method: "POST", authed: false, permission: Permissions::CREATE_USER)) {
             global $config, $page, $user;
-            if (!$config->get_bool("login_signup_enabled")) {
+            if (!$config->get_bool(UserPageConfig::SIGNUP_ENABLED)) {
                 $this->theme->display_signups_disabled($page);
                 return;
             }
@@ -312,7 +312,7 @@ public function onPageRequest(PageRequestEvent $event): void
 
         if ($event->page_matches("user/{name}")) {
             $display_user = User::by_name($event->get_arg('name'));
-            if ($display_user->id == $config->get_int("anon_id")) {
+            if ($display_user->id == $config->get_int(UserPageConfig::ANON_ID)) {
                 throw new UserNotFound("No such user");
             }
             $e = send_event(new UserPageBuildingEvent($display_user));
@@ -346,7 +346,7 @@ public function onUserPageBuilding(UserPageBuildingEvent $event): void
         if ($av) {
             $event->add_part($av, 0);
         } elseif (
-            ($config->get_string("avatar_host") == "gravatar") &&
+            ($config->get_string(AvatarConfig::HOST) == "gravatar") &&
             ($user->id == $duser->id)
         ) {
             $event->add_part(
@@ -414,8 +414,8 @@ public function onSetupBuilding(SetupBuildingEvent $event): void
         $sb = $event->panel->create_new_block("User Options");
         $sb->start_table();
         $sb->add_bool_option(UserConfig::ENABLE_API_KEYS, "Enable user API keys", true);
-        $sb->add_bool_option("login_signup_enabled", "Allow new signups", true);
-        $sb->add_bool_option("user_email_required", "Require email address", true);
+        $sb->add_bool_option(UserPageConfig::SIGNUP_ENABLED, "Allow new signups", true);
+        $sb->add_bool_option(UserPageConfig::USER_EMAIL_REQUIRED, "Require email address", true);
         $sb->add_longtext_option("login_tac", "Terms &amp; Conditions", true);
         $sb->add_choice_option(
             "user_loginshowprofile",
@@ -426,9 +426,9 @@ public function onSetupBuilding(SetupBuildingEvent $event): void
             "On log in/out",
             true
         );
-        $sb->add_choice_option("avatar_host", $hosts, "Avatars", true);
+        $sb->add_choice_option(AvatarConfig::HOST, $hosts, "Avatars", true);
 
-        if ($config->get_string("avatar_host") == "gravatar") {
+        if ($config->get_string(AvatarConfig::HOST) == "gravatar") {
             $sb->start_table_row();
             $sb->start_table_cell(2);
             $sb->add_label("<div style='text-align: center'><b>Gravatar Options</b></div>");
@@ -436,7 +436,7 @@ public function onSetupBuilding(SetupBuildingEvent $event): void
             $sb->end_table_row();
 
             $sb->add_choice_option(
-                "avatar_gravatar_type",
+                AvatarConfig::GRAVATAR_TYPE,
                 [
                     'Default' => 'default',
                     'Wavatar' => 'wavatar',
@@ -447,7 +447,7 @@ public function onSetupBuilding(SetupBuildingEvent $event): void
                 true
             );
             $sb->add_choice_option(
-                "avatar_gravatar_rating",
+                AvatarConfig::GRAVATAR_RATING,
                 ['G' => 'g', 'PG' => 'pg', 'R' => 'r', 'X' => 'x'],
                 "Rating",
                 true
@@ -502,7 +502,7 @@ public function onUserCreation(UserCreationEvent $event): void
         if (!$user->can(Permissions::CREATE_USER)) {
             throw new UserCreationException("Account creation is currently disabled");
         }
-        if (!$config->get_bool("login_signup_enabled") && !$user->can(Permissions::CREATE_OTHER_USER)) {
+        if (!$config->get_bool(UserPageConfig::SIGNUP_ENABLED) && !$user->can(Permissions::CREATE_OTHER_USER)) {
             throw new UserCreationException("Account creation is currently disabled");
         }
         if (strlen($name) < 1) {
@@ -530,7 +530,7 @@ public function onUserCreation(UserCreationEvent $event): void
             // Users who can create other users (ie, admins) are exempt
             // from the email requirement
             !$user->can(Permissions::CREATE_OTHER_USER) &&
-            ($config->get_bool("user_email_required") && empty($event->email))
+            ($config->get_bool(UserPageConfig::USER_EMAIL_REQUIRED) && empty($event->email))
         ) {
             throw new UserCreationException("Email address is required");
         }
@@ -630,11 +630,11 @@ private function page_login(string $name, string $pass): void
     private function page_logout(): void
     {
         global $page, $config;
-        $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/");
+        $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int(UserPageConfig::LOGIN_MEMORY), "/");
         if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::PURGE_COOKIE)) {
             # to keep as few versions of content as possible,
             # make cookies all-or-nothing
-            $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/");
+            $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int(UserPageConfig::LOGIN_MEMORY), "/");
         }
         log_info("user", "Logged out");
         $page->set_mode(PageMode::REDIRECT);
@@ -760,7 +760,7 @@ private function delete_user(Page $page, int $uid, bool $with_images = false, bo
         } else {
             $database->execute(
                 "UPDATE images SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id",
-                ["new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $uid]
+                ["new_owner_id" => $config->get_int(UserPageConfig::ANON_ID), "old_owner_id" => $uid]
             );
         }
 
diff --git a/ext/user/theme.php b/ext/user/theme.php
index 21bc473ab..d2dccc0d6 100644
--- a/ext/user/theme.php
+++ b/ext/user/theme.php
@@ -61,14 +61,14 @@ public function display_user_block(Page $page, User $user, array $parts): void
     public function display_signup_page(Page $page): void
     {
         global $config, $user;
-        $tac = $config->get_string("login_tac", "");
+        $tac = $config->get_string(UserPageConfig::LOGIN_TAC, "");
 
-        if ($config->get_bool("login_tac_bbcode")) {
+        if ($config->get_bool(UserPageConfig::LOGIN_TAC_BBCODE)) {
             $tac = format_text($tac);
         }
 
         $email_required = (
-            $config->get_bool("user_email_required") &&
+            $config->get_bool(UserPageConfig::USER_EMAIL_REQUIRED) &&
             !$user->can(Permissions::CREATE_OTHER_USER)
         );
 
@@ -190,7 +190,7 @@ public function create_login_block(): HTMLElement
 
         $html = emptyHTML();
         $html->appendChild($form);
-        if ($config->get_bool("login_signup_enabled") && $user->can(Permissions::CREATE_USER)) {
+        if ($config->get_bool(UserPageConfig::SIGNUP_ENABLED) && $user->can(Permissions::CREATE_USER)) {
             $html->appendChild(SMALL(A(["href" => make_link("user_admin/create")], "Create Account")));
         }