Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.
Lessons
The Settings API is WordPress's built-in system for plugin settings pages: you register a setting with a sanitising function, describe its sections and fields, and WordPress handles the form submission, nonce check, saving and error messages for you.
A hand-built settings form has to verify a nonce, check capabilities, sanitise every field, save, and redirect back with a message — and most hand-built forms miss at least one of those. The Settings API does all of them, the same way core's own settings screens do.
<?php
function gdc_books_register_settings() {
register_setting(
'gdc_books_settings_group', // Option group: ties the form to this setting.
'gdc_books_settings', // Option name in wp_options. One array for all fields.
array(
'type' => 'array',
'sanitize_callback' => 'gdc_books_sanitize_settings',
'default' => array(
'books_per_page' => 6,
'show_isbn' => 1,
'currency' => 'USD',
),
)
);
add_settings_section(
'gdc_books_display', // Section ID.
esc_html__( 'Display', 'gdc-books' ), // Heading.
'gdc_books_display_section_intro', // Intro text callback.
'gdc-books-settings' // Page slug from add_submenu_page().
);
add_settings_field(
'books_per_page',
esc_html__( 'Books per page', 'gdc-books' ),
'gdc_books_field_number',
'gdc-books-settings',
'gdc_books_display',
array( 'label_for' => 'books_per_page', 'min' => 1, 'max' => 24 )
);
add_settings_field(
'show_isbn',
esc_html__( 'Show ISBN', 'gdc-books' ),
'gdc_books_field_checkbox',
'gdc-books-settings',
'gdc_books_display',
array( 'label_for' => 'show_isbn' )
);
}
add_action( 'admin_init', 'gdc_books_register_settings' );Tip: Store all your plugin's settings in one array option, as above, rather than one option per field. It is one database row, one query, and one thing to delete on uninstall.
<?php
function gdc_books_display_section_intro() {
echo '<p>' . esc_html__( 'Control how books are listed on the front end.', 'gdc-books' ) . '</p>';
}
function gdc_books_field_number( $args ) {
$options = get_option( 'gdc_books_settings' );
$value = isset( $options[ $args['label_for'] ] ) ? (int) $options[ $args['label_for'] ] : 6;
printf(
'<input type="number" id="%1$s" name="gdc_books_settings[%1$s]" value="%2$d" min="%3$d" max="%4$d" class="small-text">',
esc_attr( $args['label_for'] ),
$value,
(int) $args['min'],
(int) $args['max']
);
}
function gdc_books_field_checkbox( $args ) {
$options = get_option( 'gdc_books_settings' );
$checked = ! empty( $options[ $args['label_for'] ] );
printf(
'<input type="checkbox" id="%1$s" name="gdc_books_settings[%1$s]" value="1" %2$s>',
esc_attr( $args['label_for'] ),
checked( $checked, true, false )
);
}Field names follow the pattern option_name[field], so the browser submits them as one array — exactly the shape you registered.
<?php
function gdc_books_sanitize_settings( $input ) {
$clean = array();
$per_page = isset( $input['books_per_page'] ) ? absint( $input['books_per_page'] ) : 6;
if ( $per_page < 1 || $per_page > 24 ) {
add_settings_error(
'gdc_books_settings',
'gdc_books_per_page',
esc_html__( 'Books per page must be between 1 and 24.', 'gdc-books' )
);
$per_page = 6;
}
$clean['books_per_page'] = $per_page;
// An unchecked checkbox is simply absent from the submitted data.
$clean['show_isbn'] = empty( $input['show_isbn'] ) ? 0 : 1;
$allowed = array( 'USD', 'EUR', 'INR', 'GBP' );
$clean['currency'] = in_array( $input['currency'] ?? '', $allowed, true ) ? $input['currency'] : 'USD';
return $clean;
}Build a new array from known keys. Never return $input itself — that would save any extra field an attacker added to the form.
<?php
function gdc_books_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'gdc_books_settings_group' ); // Nonce, action and option page fields.
do_settings_sections( 'gdc-books-settings' ); // Every section and field for this page.
submit_button();
?>
</form>
</div>
<?php
}The form posts to options.php — core's own handler — which verifies the nonce from settings_fields(), checks the capability, runs your sanitize callback and saves. Messages added with add_settings_error() appear automatically on pages under the Settings menu; on a custom top-level page, call settings_errors() yourself above the form.
<select>.get_option( 'gdc_books_settings' ) and use books_per_page as your shortcode's default.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.