How to create a WordPress Theme Options Page

Themefuse Having an options page for your WordPress theme is a great way to increase the theme’s extensibility, usability and ease of operation. At first glance, implementing such a page may seem a hard task and not so long ago it was; however, now with the new WordPress Settings API introduced in version 2.7, creating an options page has become a task as simple as registering the required settings and then calling them where required.

The Settings API was created in an effort to streamline the  calling for options in WordPress themes and plugins and also, to provide an easy, more secure way to do it. Here, I will present a very simple way to implement this functionality, but that meets all guidelines from the WordPress documentation.

You see, the problem here is that this API exists for almost 2 years and people just tend to stick to what they know and do it   the old way. But that’s wrong for the following reasons:

  • It’s hard
  • It’s not secure
  • The code is a mess

While the Settings API offers:

  • Easyness
  • Hardened security
  • Compatibility with the WordPress Core

So down to business, here’s how we’ll create together a sample options page for your WordPress theme:

Let’s say we’ll make a page with five options: An option that changes the footer copyright text, an intro text to show on the front page, a drop down list to select a category to show as featured on the front page, some radio buttons to choose how we want to main layout to look like, and a checkbox whether to show the autor’s credit links.

At the end, we are going to bundle the entire code into a functioning child theme for Twenty Ten called “Settings API”.

We are going to prefix all the function and option names used here with a shortname of our theme, in this case “Settings API”, hence “sa_”. This is to avoid conflicting with plugins that may use similar names.

First, we’ll need to register the options we’ll use, and we’ll do this with the function register_setting, here’s how:

function sa_register_settings() {
	register_setting( 'sa_theme_options', 'sa_options', 'sa_validate_options' );
}

add_action( 'admin_init', 'sa_register_settings' );

The register_setting function needs to be called within a function hooked to the admin_init action or else it will report a call to undefined function error.

Note: We have registered a single setting, which we will use as an array containing all the settings we need for the theme. This is the recommended way to do it, but individual settings can be registered as well.

Now, here’s what all these parameters mean:

The first parameter is the is the so called options group. This can be set to any string, but all settings belonging to this group will need to be called within the same form, or the ones missing will be voided.

The second parameter is the actual setting name, this can also be given any name and it’s the name the setting will be called with.

The third parameter is the name of the function that sanitizes the submitted value, to make sure it’s actually within the declared values and to avoid vulnerabilities such as cross-site scripting also knows as XSS. This step is optional but highly recommended. We’ll get into this later.

So, that’s all that was necessary to declare these options as existent. At this point the options can be called in the theme with the function get_option:

$settings = get_option('sa_options');

Although at this point the function will not return any errors, it won’t return any value as well, because an option for footer_copyright has not been given yet and a default value has not been set. That’s why we need to seet some default values, to make the options usable, before the user touches the options form. We’ll do this within an array:

$sa_options = array(	'footer_copyright' => '© ' . date('Y') . get_bloginfo('name'),
	'intro_text' => '',
	'featured_cat' => 0,
	'layout_view' => 'fixed',
	'author_credits' => true
);

We have given the footer copyright notice a default “© 2011 Site Name”, left the intro text blank since maybe the owner doesn’t want to display anything, set no default featured category,  the layout to fixed and the author credits to show.

Giving these values in an array doesn’t actually make them default for these settings, just stores them for later use. The way we pass the default value to the option, is by calling it with the default value as the second parameter:

$settings = get_option( 'sa_options', $sa_options )

Now that we have the default values, we’ll also need to declare the available values, the ones that will show off in the options form, for the options that have multiple choices. Since we’re using a form to submit the options, the most convenient way to do this is by making each value into an array, that has both the input’s value and label. Here’s how:

For the featured category, we may want to store all the existing categories into an array:

$sa_categories[0] = array(
	'value' => 0,
	'label' => ''
); // Void first array key to disable feature
$sa_cats = get_categories();
foreach( $sa_cats as $sa_cat ) :
	$sa_categories[$sa_cat->cat_ID] = array(
		'value' => $sa_cat->cat_ID,
		'label' => $sa_cat->cat_name
	);
endforeach;

An now we have all the categories’ IDs and names stored in the $categories array.

$sa_layouts = array(
	'fixed' => array(
		'value' => 'fixed',
		'label' => 'Fixed Layout'
	),
	'fluid' => array(
		'value' => 'fluid',
		'label' => 'Fluid Layout'
	),
);

And the desired layout options in the $layouts array.

Now we’re ready to create the theme options page. First we’re going to register it with the function add_theme_page:

function sa_theme_options() {
	add_theme_page( 'Theme Options', 'Theme Options', 'edit_theme_options', 'theme_options', 'theme_options_page' );
}

add_action( 'admin_menu', 'sa_theme_options' );

The function add_theme_page needs to be called within a function hooked to the admin_menu action or else it will report a call to undefined function error. This function will add a submenu item to the Appearance menu of the admin panel. This is the recommended location for theme options pages, and the one people are used to. Here’s what all the parameters mean:

The first parameter is the document title, the one on the browser’s title bar, for the options page.

The second parameter is the name of the menu item that will appear under Appearance.

The third parameter is the capability needed by the user to access the page. We have set this to ‘edit_theme_options’ since this is what we’re doing.

The fourth parameter is the options page slug, the one that will appear in the page’s URL.

And finally the last parameter is the call to the function that generates the page, and now we’re getting to that.

For the options page we are going to use the same layout as standard admin pages inside WordPress to preserve consistency:

function sa_theme_options_page() {
	global $sa_options, $sa_categories, $sa_layouts;

	if ( ! isset( $_REQUEST['updated'] ) )
	$_REQUEST['updated'] = false; // This checks whether the form has just been submitted. ?>

	<div>

	<?php screen_icon(); echo "<h2>" . get_current_theme() . __( ' Theme Options' ) . "</h2>";
	// This shows the page's name and an icon if one has been provided ?>

	<?php if ( false !== $_REQUEST['updated'] ) : ?>
	<div><p><strong><?php _e( 'Options saved' ); ?></strong></p></div>
	<?php endif; // If the form has just been submitted, this shows the notification ?>

	<form method="post" action="options.php">

	<?php $settings = get_option( 'sa_options', $sa_options ); ?>

	<?php settings_fields( 'sa_theme_options' );
	/* This function outputs some hidden fields required by the form,
	including a nonce, a unique number used to ensure the form has been submitted from the admin page
	and not somewhere else, very important for security */ ?>

	<table><!-- Grab a hot cup of coffee, yes we're using tables! -->

	<tr valign="top"><th scope="row"><label for="footer_copyright">Footer Copyright Notice</label></th>
	<td>
	<input id="footer_copyright" name="sa_options[footer_copyright]" type="text" value="<?php  esc_attr_e($settings['footer_copyright']); ?>" />
	</td>
	</tr>

	<tr valign="top"><th scope="row"><label for="intro_text">Intro Text</label></th>
	<td>
	<textarea id="intro_text" name="sa_options[intro_text]" rows="5" cols="30"><?php echo stripslashes($settings['intro_text']); ?></textarea>
	</td>
	</tr>

	<tr valign="top"><th scope="row"><label for="featured_cat">Featured Category</label></th>
	<td>
	<select id="featured_cat" name="sa_options[featured_cat]">
	<?php
	foreach ( $categories as $category ) :
		$label = $category['label'];
		$selected = '';
		if ( $category['value'] == $settings['featured_cat'] )
			$selected = 'selected="selected"';
		echo '<option style="padding-right: 10px;" value="' . esc_attr( $category['value'] ) . '" ' . $selected . '>' . $label . '</option>';
	endforeach;
	?>
	</select>
	</td>
	</tr>

	<tr valign="top"><th scope="row">Layout View</th>
	<td>
	<?php foreach( $layouts as $layout ) : ?>
	<input type="radio" id="<?php echo $layout['value']; ?>" name="sa_options[layout_view]" value="<?php esc_attr_e( $layout['value'] ); ?>" <?php checked( $settings['layout_view'], $layout['value'] ); ?> />
	<label for="<?php echo $layout['value']; ?>"><?php echo $layout['label']; ?></label><br />
	<?php endforeach; ?>
	</td>
	</tr>

	<tr valign="top"><th scope="row">Author Credits</th>
	<td>
	<input type="checkbox" id="author_credits" name="sa_options[author_credits]" value="1" <?php checked( true, $settings['author_credits'] ); ?> />
	<label for="author_credits">Show Author Credits</label>
	</td>
	</tr>

	</table>

	<p><input type="submit" value="Save Options" /></p>

	</form>

	</div>

	<?php
}

Here’s what we’ve done above:

  • We’re checking if the form has just been submitted and display a notification the options have been saved
  • We’re opening the form
  • We’re calling the function settings_fields (the passed parameter must be the same as the options group) to output some required hidden fields, including a nonce. A nonce (short for “number used once”) is a unique number passes along with the form to ensure is has actually been submitted from the options page. You can read more about Nonces at the Codex.
  • Then, we call each option and pass it on to the input fields. Notice we are not directly echoing the values but escaping them through functions like esc_attr, this is the recommended way.

Important Note: The name of the input field must be the same with the name of the option being submitted.

Now there’s one more thing we’ll do to make the options values more secure: we’re going to validate each input and make sure it’s value is within the allowed ranges. For this we declare a sanitation function, which is called automatically when the form is submitted, with the input value passed as an argument. Here it is:

function sa_validate_options( $input ) {
	global $sa_options, $sa_categories, $sa_layouts;

	$settings = get_option( 'sa_options', $sa_options );

	// We strip all tags from the text field, to avoid vulnerablilties like XSS
	$input['footer_copyright'] = wp_filter_nohtml_kses( $input['footer_copyright'] );

	// We strip all tags from the text field, to avoid vulnerablilties like XSS
	$input['intro_text'] = wp_filter_post_kses( $input['intro_text'] );

	// We select the previous value of the field, to restore it in case an invalid entry has been given
	$prev = $settings['featured_cat'];
	// We verify if the given value exists in the categories array
	if ( !array_key_exists( $input['featured_cat'], $categories ) )
	$input['featured_cat'] = $prev;

	// We select the previous value of the field, to restore it in case an invalid entry has been given
	$prev = $settings['layout_view'];
	// We verify if the given value exists in the layouts array
	if ( !array_key_exists( $input['layout_view'], $layouts ) )
	$input['layout_view'] = $prev;

	// If the checkbox has not been checked, we void it
	if ( ! isset( $input['author_credits'] ) )
	$input['author_credits'] = null;
	// We verify if the input is a boolean value
	$input['author_credits'] = ( $input['author_credits'] == 1 ? 1 : 0 );

	return $input;
}

Standard PHP functions can also be called as sanitation functions, as for example absint to verify if the value is an integer greater than 0. More information about data sanitation can be fount at the Codex.

Here we have it: a simple, secure theme options page ready to go!

Now let’s see some usage of the options. First, we’re going to store the options in a variable called $sa_settings:

global $sa_options;
$sa_settings = get_option( 'sa_options', $sa_options );

Here’s how the section of the footer displaying the copyright notification should look like:

<div id="footer">
<?php echo $sa_settings['footer_copyright']; ?>
</div>

This will simply output what has been given in the theme options, or the default.

Here’s how the intro text section should be called:

<?php if( $sa_settings['intro_text'] != '' ) : ?>
<div class="intro">
<?php echo $sa_settings['intro_text']; ?>
</div>
<?php endif; ?>

Displaying the featured category:

<?php if( $sa_settings['featured_cat'] ) : ?>
<div class="featured">
<?php query_posts('cat=' . $sa_settings['featured_cat'] ); ?>
<?php if(have_posts()) : while(have_posts()) : the_post(); ?>
// The Loop
<?php endwhile; endif; rewind_posts(); wp_reset_query(); ?>
</div>
<?php endif; ?>

Displaying the layout:

#wrapper {
	<?php if( $sa_settings['layout_view'] == 'fixed' ) : ?>
	width: 960px;
	<?php else: ?>
	padding: 0 20px;
	width: 100%;
	<?php endif; ?>
}

Showing credit links:

<?php if( $sa_settings['author_credits'] ) : ?>
<a href="http://www.example.com/">Created by Author</a>
<?php endif; ?>

These functions have been bundled in a nice Twenty Ten child theme called “Settings API”, you can download it below:

Download Source Code

Hope this helps in building better, more secure options pages.

If you enjoyed this you may also like

Leave a Reply