Settings Page Generator
Menu registration, the Settings API wiring, a real sanitiser per field type, and the screen it all renders into — laid out the way core expects.
The page
The conventional home for plugin settings.
Menu icon
Appears as its own sidebar item. The first submenu item repeats the page, so many plugins rename it with a second add_submenu_page() call.
Storage & naming
Every field lives in one row: acme_toolkit_options. One register_setting() call, one sanitiser, one row to delete on uninstall.
Sections
2 sections · one tab each
Fields
5 fields · one option
value:Label pairs, comma separated. Values are whitelisted in the sanitiser.
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io). /** * Default values for every field. * * @return array */ function acme_default_options() { return array( 'enabled' => true, 'mode' => 'fast', 'accent' => '#2271b1', 'api_key' => '', 'cache_ttl' => 3600, ); } /** * Saved options, merged over the defaults so no key is ever missing. * * @return array */ function acme_get_options() { return wp_parse_args( (array) get_option( 'acme_toolkit_options', array() ), acme_default_options() ); } /** * Add the page to the admin menu. */ function acme_add_settings_page() { $hook = add_menu_page( __( 'Acme Toolkit Settings', 'acme-toolkit' ), __( 'Acme Toolkit', 'acme-toolkit' ), 'manage_options', 'acme-toolkit', 'acme_render_settings_page', 'dashicons-admin-generic', 80 ); acme_settings_hook( $hook ); } add_action( 'admin_menu', 'acme_add_settings_page' ); /** * Remember the screen hook suffix add_menu_page() handed back. * * @param string|null $hook Hook suffix to store, or null to read it. * @return string */ function acme_settings_hook( $hook = null ) { static $stored = ''; if ( null !== $hook ) { $stored = (string) $hook; } return $stored; } /** * Register the setting, its sections and its fields. */ function acme_register_settings() { register_setting( 'acme_toolkit_group', 'acme_toolkit_options', array( 'type' => 'array', 'sanitize_callback' => 'acme_sanitize_options', 'default' => acme_default_options(), 'show_in_rest' => false, ) ); add_settings_section( 'acme_section_general', __( 'General', 'acme-toolkit' ), 'acme_render_section', 'acme-toolkit-general' ); add_settings_field( 'enabled', __( 'Enable the toolkit', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'enabled', 'type' => 'checkbox', 'label_for' => 'enabled', 'default' => true, 'placeholder' => '', 'description' => __( 'Turn everything off without deactivating the plugin.', 'acme-toolkit' ), 'choices' => array(), ) ); add_settings_field( 'mode', __( 'Mode', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'mode', 'type' => 'select', 'label_for' => 'mode', 'default' => 'fast', 'placeholder' => '', 'description' => __( 'Fast skips the extra validation pass.', 'acme-toolkit' ), 'choices' => array( 'fast' => __( 'Fast', 'acme-toolkit' ), 'safe' => __( 'Safe', 'acme-toolkit' ), ), ) ); add_settings_field( 'accent', __( 'Accent colour', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'accent', 'type' => 'color', 'label_for' => 'accent', 'default' => '#2271b1', 'placeholder' => '#2271b1', 'description' => '', 'choices' => array(), ) ); add_settings_section( 'acme_section_api', __( 'API', 'acme-toolkit' ), 'acme_render_section', 'acme-toolkit-api' ); add_settings_field( 'api_key', __( 'API key', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-api', 'acme_section_api', array( 'key' => 'api_key', 'type' => 'text', 'label_for' => 'api_key', 'default' => '', 'placeholder' => 'sk_live_…', 'description' => __( 'Found under Account → Developers.', 'acme-toolkit' ), 'choices' => array(), ) ); add_settings_field( 'cache_ttl', __( 'Cache lifetime', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-api', 'acme_section_api', array( 'key' => 'cache_ttl', 'type' => 'number', 'label_for' => 'cache_ttl', 'default' => 3600, 'placeholder' => '3600', 'description' => __( 'Seconds to keep API responses.', 'acme-toolkit' ), 'choices' => array(), ) ); } add_action( 'admin_init', 'acme_register_settings' ); /** * Sanitise the submitted values. Anything not listed here never reaches the database. * * @param mixed $input Raw value from the form. * @return array */ function acme_sanitize_options( $input ) { $input = (array) $input; $output = acme_default_options(); $output['enabled'] = ! empty( $input['enabled'] ); $output['mode'] = isset( $input['mode'] ) && in_array( $input['mode'], array( 'fast', 'safe' ), true ) ? sanitize_key( $input['mode'] ) : 'fast'; $output['accent'] = isset( $input['accent'] ) && sanitize_hex_color( $input['accent'] ) ? sanitize_hex_color( $input['accent'] ) : '#2271b1'; $output['api_key'] = isset( $input['api_key'] ) ? sanitize_text_field( $input['api_key'] ) : ''; $output['cache_ttl'] = isset( $input['cache_ttl'] ) ? absint( $input['cache_ttl'] ) : 3600; return $output; } /** * Print the blurb under a section heading. * * @param array $args Section arguments from add_settings_section(). */ function acme_render_section( $args ) { $descriptions = array( 'acme_section_general' => __( 'How the toolkit behaves on the front end.', 'acme-toolkit' ), 'acme_section_api' => __( 'Credentials for the remote service.', 'acme-toolkit' ), ); if ( ! empty( $descriptions[ $args['id'] ] ) ) { echo '<p>' . esc_html( $descriptions[ $args['id'] ] ) . '</p>'; } } /** * Print one field. Every type routes through here so escaping lives in one place. * * @param array $args Field arguments from add_settings_field(). */ function acme_render_field( $args ) { $options = acme_get_options(); $key = $args['key']; $name = 'acme_toolkit_options[' . $key . ']'; $value = isset( $options[ $key ] ) ? $options[ $key ] : $args['default']; switch ( $args['type'] ) { case 'checkbox': printf( '<input type="checkbox" id="%1$s" name="%2$s" value="1"%3$s />', esc_attr( $key ), esc_attr( $name ), checked( (bool) $value, true, false ) ); break; case 'select': echo '<select id="' . esc_attr( $key ) . '" name="' . esc_attr( $name ) . '">'; foreach ( $args['choices'] as $choice => $label ) { printf( '<option value="%1$s"%2$s>%3$s</option>', esc_attr( $choice ), selected( $value, $choice, false ), esc_html( $label ) ); } echo '</select>'; break; default: printf( '<input type="%1$s" id="%2$s" name="%3$s" value="%4$s" class="regular-text" placeholder="%5$s" />', esc_attr( $args['type'] ), esc_attr( $key ), esc_attr( $name ), esc_attr( $value ), esc_attr( $args['placeholder'] ) ); break; } if ( ! empty( $args['description'] ) ) { echo '<p class="description">' . esc_html( $args['description'] ) . '</p>'; } } /** * Render the settings screen. */ function acme_render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tabs = array( 'general' => __( 'General', 'acme-toolkit' ), 'api' => __( 'API', 'acme-toolkit' ), ); $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $tabs[ $active_tab ] ) ) { $active_tab = 'general'; } ?> <div class="wrap"> <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> <?php settings_errors( 'acme_toolkit_options' ); ?> <h2 class="nav-tab-wrapper"> <?php foreach ( $tabs as $tab_key => $tab_label ) : ?> <a href="<?php echo esc_url( add_query_arg( array( 'page' => 'acme-toolkit', 'tab' => $tab_key ), admin_url( 'admin.php' ) ) ); ?>" class="nav-tab <?php echo $active_tab === $tab_key ? 'nav-tab-active' : ''; ?>" > <?php echo esc_html( $tab_label ); ?> </a> <?php endforeach; ?> </h2> <form action="options.php" method="post"> <?php settings_fields( 'acme_toolkit_group' ); do_settings_sections( 'acme-toolkit-' . $active_tab ); submit_button(); ?> </form> </div> <?php } /** * Load assets on this screen only — never on every admin page. * * @param string $hook_suffix Current admin screen. */ function acme_admin_assets( $hook_suffix ) { if ( acme_settings_hook() !== $hook_suffix ) { return; } wp_enqueue_style( 'acme-toolkit-admin', plugins_url( 'assets/admin.css', __FILE__ ), array(), '1.0.0' ); } add_action( 'admin_enqueue_scripts', 'acme_admin_assets' ); /* * uninstall.php — sits next to the plugin file and runs when the plugin is deleted. * * defined( 'WP_UNINSTALL_PLUGIN' ) || exit; * delete_option( 'acme_toolkit_options' ); */