Independent Editorial DeskWordPress Releases, Builds, and Operations
Back to Archive
Hardening NotesImplementation Notes

WordPress Settings API Example: Build a Safer Plugin Settings Page

Build a safer WordPress plugin settings page with register_setting, sanitization callbacks, capability checks, escaping, and predictable defaults.

Published

April 17, 2026

Reading Time

2 min read

Updated

April 17, 2026

Abstract WordPress settings controls representing safe plugin configuration.
Control LedgerHardening Notes

Hardening Notes

Baselines, access reduction, and default settings that stand up in production.

Best For

Teams preparing, launching, or maintaining WordPress as a backend service in a production stack.

Primary Topics

Hardening NotesImplementation Notes

Editorial Focus

Control Ledger: Baselines, access reduction, and default settings that stand up in production. Updated on April 17, 2026.

Full Report

Last reviewed: April 17, 2026

A WordPress Settings API example is useful because many plugins start with a simple option and then quietly turn into fragile admin code. The common bad pattern is direct $_POST handling inside an options page, missing capability checks, missing nonces, unescaped values, and no clear sanitization boundary.

This guide shows a small plugin settings page using the WordPress Settings API. The goal is not to build a complex admin interface. The goal is to create a safe baseline that stores options predictably and can survive production use.

Who this guide is for

This article is for WordPress plugin developers and teams maintaining custom admin tools. If your plugin needs toggles, API endpoint URLs, display messages, feature flags, or integration settings, start here before building custom form handling.

What the Settings API gives you

The Settings API helps register options, render fields, output nonce fields, group settings, and run sanitization callbacks. It does not remove the need for careful escaping, capability checks, or a clear data model, but it gives you the correct WordPress plumbing.

Plugin example: a safer settings page

The example below creates a settings page under Settings -> VulnWP Alerts. It stores an enabled flag and a short message in a single option array. The page is only available to administrators who can manage options.

<?php
/**
 * Plugin Name: VulnWP Alert Settings
 * Description: Example Settings API page with sanitization and escaping.
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const VULNWP_ALERT_OPTION = 'vulnwp_alert_settings';

add_action( 'admin_menu', 'vulnwp_alert_add_settings_page' );
add_action( 'admin_init', 'vulnwp_alert_register_settings' );

function vulnwp_alert_add_settings_page() {
	add_options_page(
		__( 'VulnWP Alerts', 'vulnwp-alert-settings' ),
		__( 'VulnWP Alerts', 'vulnwp-alert-settings' ),
		'manage_options',
		'vulnwp-alert-settings',
		'vulnwp_alert_render_settings_page'
	);
}

function vulnwp_alert_register_settings() {
	register_setting(
		'vulnwp_alert_group',
		VULNWP_ALERT_OPTION,
		array(
			'type'              => 'array',
			'sanitize_callback' => 'vulnwp_alert_sanitize_settings',
			'default'           => array(
				'enabled' => false,
				'message' => '',
			),
		)
	);

	add_settings_section(
		'vulnwp_alert_main',
		__( 'Alert Display', 'vulnwp-alert-settings' ),
		'vulnwp_alert_render_section_intro',
		'vulnwp-alert-settings'
	);

	add_settings_field(
		'enabled',
		__( 'Enable alert', 'vulnwp-alert-settings' ),
		'vulnwp_alert_render_enabled_field',
		'vulnwp-alert-settings',
		'vulnwp_alert_main'
	);

	add_settings_field(
		'message',
		__( 'Alert message', 'vulnwp-alert-settings' ),
		'vulnwp_alert_render_message_field',
		'vulnwp-alert-settings',
		'vulnwp_alert_main'
	);
}

function vulnwp_alert_sanitize_settings( $input ) {
	$input = is_array( $input ) ? $input : array();

	return array(
		'enabled' => ! empty( $input['enabled'] ),
		'message' => isset( $input['message'] )
			? sanitize_text_field( wp_unslash( $input['message'] ) )
			: '',
	);
}

function vulnwp_alert_get_settings() {
	$defaults = array(
		'enabled' => false,
		'message' => '',
	);

	$settings = get_option( VULNWP_ALERT_OPTION, $defaults );

	return wp_parse_args( $settings, $defaults );
}

function vulnwp_alert_render_section_intro() {
	echo '<p>' . esc_html__( 'Configure the frontend alert message.', 'vulnwp-alert-settings' ) . '</p>';
}

function vulnwp_alert_render_enabled_field() {
	$settings = vulnwp_alert_get_settings();

	printf(
		'<label><input type="checkbox" name="%1$s[enabled]" value="1" %2$s> %3$s</label>',
		esc_attr( VULNWP_ALERT_OPTION ),
		checked( true, (bool) $settings['enabled'], false ),
		esc_html__( 'Show the alert', 'vulnwp-alert-settings' )
	);
}

function vulnwp_alert_render_message_field() {
	$settings = vulnwp_alert_get_settings();

	printf(
		'<input type="text" class="regular-text" name="%1$s[message]" value="%2$s" maxlength="140">',
		esc_attr( VULNWP_ALERT_OPTION ),
		esc_attr( $settings['message'] )
	);
}

function vulnwp_alert_render_settings_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You do not have permission to access this page.', 'vulnwp-alert-settings' ) );
	}
	?>
	<div class="wrap">
		<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
		<form method="post" action="options.php">
			<?php
			settings_fields( 'vulnwp_alert_group' );
			do_settings_sections( 'vulnwp-alert-settings' );
			submit_button();
			?>
		</form>
	</div>
	<?php
}

Why this is safer than manual form handling

The form posts to options.php, and settings_fields() outputs the correct hidden fields and nonce for the registered settings group. The stored values pass through vulnwp_alert_sanitize_settings(), which is the single input boundary for the option.

That still leaves output escaping as a separate requirement. Values printed into HTML attributes use esc_attr(). Text printed into normal HTML should use esc_html(). Sanitizing on save and escaping on output are related, but they are not the same control.

Checklist for production settings pages

  • Use add_options_page() or another admin menu function with the narrowest useful capability.
  • Use register_setting() with a sanitization callback.
  • Keep all option sanitization in one function per option group.
  • Use settings_fields() and do_settings_sections() instead of hand-building hidden fields.
  • Escape every value when rendering form fields.
  • Store related settings in one option array when they are loaded together.
  • Document default values so empty installs behave predictably.

Common mistakes

  • Trusting admin input. Administrators can still paste unsafe values, and compromised admin sessions are a real incident path.
  • Using sanitize_text_field() for everything. URLs, integers, booleans, emails, and HTML each need different handling.
  • Escaping only on save. Escaping belongs at output time because output context changes.
  • Skipping capability checks. Menu capabilities control visibility, but explicit checks make the page harder to misuse.
  • Spreading option names across the codebase. Constants reduce typos and make migrations easier.

Adding frontend output safely

If the option controls frontend output, read the stored option and escape for the final context. This example prints a simple message only when the toggle is enabled.

add_action( 'wp_footer', 'vulnwp_alert_render_frontend_message' );

function vulnwp_alert_render_frontend_message() {
	$settings = vulnwp_alert_get_settings();

	if ( empty( $settings['enabled'] ) || '' === $settings['message'] ) {
		return;
	}

	printf(
		'<div class="vulnwp-alert" role="status">%s</div>',
		esc_html( $settings['message'] )
	);
}

Where this fits with plugin security

A settings page is often the first privileged surface in a plugin. Treat it like any other write action: capability boundary, nonce boundary, sanitization, escaping, and predictable defaults. If the setting later controls REST API output, cron behavior, email delivery, or external integrations, this baseline matters even more.

Related reading

Pair this with our WordPress nonce example for custom admin actions that cannot use options.php, and with the administrator access audit checklist for reviewing who can change production settings.

References and further reading

Popular Guides

Popular WordPress guides to read next.

These articles connect recurring production concerns: implementation details, updates, troubleshooting, recovery paths, and operational cleanup.

Continue Reading

More from the archive.

Diagnostic dashboard scene representing a WordPress Site Health review before major updates.
01Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Site Health Check Before Major Updates: What to Review First

A pre-update WordPress Site Health checklist covering loopbacks, connectivity, debug settings, and environment readiness.

Structured data and route review scene representing permalink validation after a WordPress migration.
02Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Permalink Checklist After Migration: Catch URL Problems Early

A post-migration WordPress permalink checklist for checking rewrite rules, post URLs, archives, and redirect noise.

Technical media workspace representing image preparation and optimization before upload to WordPress.
03Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Image Optimization Checklist: What to Fix Before Upload

A practical WordPress image optimization checklist covering dimensions, compression, formats, and Media settings before upload.