All WordPress Exercises

Create a post and a page with WP-CLI

Description: Create a published post titled My first post with some text, and a published page titled Contact. Then list the posts and the pages with their ID and status.

Hint:

  • Use wp post create with --post_type=page for the page.
  • The flag --porcelain prints only the new ID.
  • wp post list --post_type=page lists the pages.

Solution

Terminal output

$ wp post create –post_title="My first post" –post_content="Hello WordPress!" –post_status=publish –porcelain
5
$ wp post create –post_type=page –post_title="Contact" –post_status=publish –porcelain
6
$ wp post list –post_type=post –fields=ID,post_title,post_status
ID post_title post_status
5 My first post publish
1 Hello world! publish
$ wp post list –post_type=page –fields=ID,post_title,post_status
ID post_title post_status
6 Contact publish
2 Sample Page publish
3 Privacy Policy draft

How it works

  • wp post create inserts a row in the wp_posts table. The post_type decides if it is a post or a page.
  • The default type is post, so only the page needs --post_type=page.
  • The list shows also the sample content of a fresh site.

Read WordPress Posts and WordPress Pages.

Add categories, a child category and a tag to a post

Description: Create a category Travel with a child category India, and a tag tips. Then create a published post Visit Jaipur in the category India with the tag tips, and list the categories with the post count.

Hint:

  • wp term create category "Name" --parent=ID makes a child category.
  • wp post create has the flags --post_category and --tags_input.

Solution

Terminal output

$ wp term create category "Travel" –porcelain
2
$ wp term create category "India" –parent=2 –porcelain
3
$ wp term create post_tag "tips" –porcelain
4
$ wp post create –post_title="Visit Jaipur" –post_status=publish –post_category=3 –tags_input=tips –porcelain
5
$ wp term list category –fields=term_id,name,parent,count
term_id name parent count
3 India 2 1
2 Travel 0 0
1 Uncategorized 0 1
$ wp term list post_tag –fields=term_id,name,count
term_id name count
4 tips 1

How it works

  • The first category you make gets the ID 2, because ID 1 is Uncategorized. We used the ID of Travel as the parent of India.
  • The post is in India only, so the count of India is 1 and the count of Travel stays 0, as WordPress does not count child posts in the parent by default here.

Read Categories and Tags.

Change the site title and tagline and check them on the site

Description: Change the site title to Tutorials Class Demo and the tagline to Learn WordPress step by step. Then check that the new title is in the <title> tag of the home page.

Hint:

  • The options are blogname and blogdescription.
  • In the dashboard it is Settings > General.

Solution

Terminal output

$ wp option update blogname "Tutorials Class Demo"
Success: Updated 'blogname' option.
$ wp option update blogdescription "Learn WordPress step by step"
Success: Updated 'blogdescription' option.
$ wp option get blogname
Tutorials Class Demo
$ curl -s http://example.com/ | grep -o "<title>[^<]*</title>"
<title>Tutorials Class Demo &#8211; Learn WordPress step by step</title>

How it works

  • Settings are rows in the wp_options table. wp option update changes one row.
  • The theme prints the site title and the tagline in the title tag, so the home page shows the new text at once.

Read WordPress Settings.

Set a static front page and a posts page

Description: Create two pages, Home and News. Make Home the front page of the site and News the page which lists the blog posts. Check the setting.

Hint:

  • The options are show_on_front, page_on_front and page_for_posts.

Solution

Terminal output

$ wp post create –post_type=page –post_title="Home" –post_status=publish –porcelain
5
$ wp post create –post_type=page –post_title="News" –post_status=publish –porcelain
6
$ wp option update show_on_front page
Success: Updated 'show_on_front' option.
$ wp option update page_on_front 5
Success: Updated 'page_on_front' option.
$ wp option update page_for_posts 6
Success: Updated 'page_for_posts' option.
$ curl -s -o /dev/null -w "%{http_code}\n" http://example.com/news/
200
$ wp option get page_on_front
5

How it works

  • show_on_front is page when you use a static page. The two other options keep the page IDs.
  • The News page now shows the latest posts at /news/.

Read WordPress Pages.

Create two users and compare their capabilities

Description: Create a user meera with the role editor and a user karan with the role subscriber. Print for both users if they can edit_others_posts and publish_posts.

Hint:

  • Use wp user create with --role.
  • The PHP function user_can( $user_id, $capability ) checks a capability.

Solution

Terminal output

$ wp user create meera meera@example.com –role=editor –user_pass=Pass#1234 –porcelain
2
$ wp user create karan karan@example.com –role=subscriber –user_pass=Pass#1234 –porcelain
3
$ wp user list –fields=ID,user_login,roles
ID user_login roles
1 admin administrator
3 karan subscriber
2 meera editor
$ wp eval 'foreach ( array( 2 => "meera", 3 => "karan" ) as $id => $name ) { foreach ( array( "edit_others_posts", "publish_posts" ) as $cap ) { echo $name, " ", $cap, ": ", user_can( $id, $cap ) ? "yes" : "no", "\n"; } }'
meera edit_others_posts: yes
meera publish_posts: yes
karan edit_others_posts: no
karan publish_posts: no

How it works

  • The role gives a set of capabilities. An editor can publish and edit the posts of other users, and a subscriber can do neither.
  • user_can() checks any user by ID, while current_user_can() checks the user who is logged in.

Read Users and Roles.

Change the permalink structure and print a post URL

Description: Create a post Permalink practice dated 10 February 2026. Print its URL with the Post name structure. Then change the structure to /%year%/%monthnum%/%postname%/ and print the URL again.

Hint:

  • wp rewrite structure '/%postname%/' sets the structure.
  • get_permalink( $id ) gives the URL of a post.

Solution

Terminal output

$ wp post create –post_title="Permalink practice" –post_status=publish –post_date="2026-02-10 09:00:00" –porcelain
5
$ wp rewrite structure '/%postname%/'
Success: Rewrite structure set.
Success: Rewrite rules flushed.
$ wp eval 'echo get_permalink( 5 );'
http://example.com/permalink-practice/
$ wp rewrite structure '/%year%/%monthnum%/%postname%/'
Success: Rewrite structure set.
Success: Rewrite rules flushed.
$ wp eval 'echo get_permalink( 5 );'
http://example.com/2026/02/permalink-practice/
$ curl -s -o /dev/null -w "%{http_code}\n" http://example.com/2026/02/permalink-practice/
200

How it works

  • The structure is saved in the option permalink_structure. The command also flushes the rewrite rules.
  • The new URL works at once (code 200). If a URL gives 404 after a change, flush the rules.

Read WordPress Permalinks.

Create a navigation menu with three links

Description: Create a menu named Top Menu with three items: Home (the home page address), About (a page of your site) and Docs (the link https://wordpress.org/documentation/). List the items.

Hint:

  • wp menu create, wp menu item add-post and wp menu item add-custom.

Solution

Terminal output

$ wp post create –post_type=page –post_title="About" –post_status=publish –porcelain
5
$ wp menu create "Top Menu"
Success: Created menu 2.
$ wp menu item add-custom top-menu "Home" http://example.com/
Success: Menu item added.
$ wp menu item add-post top-menu 5 –title="About"
Success: Menu item added.
$ wp menu item add-custom top-menu "Docs" https://wordpress.org/documentation/
Success: Menu item added.
$ wp menu item list top-menu –fields=db_id,title,type,link
db_id title type link
6 Home custom http://example.com/
7 About post_type http://example.com/about/
8 Docs custom https://wordpress.org/documentation/

How it works

  • A menu is a term of the taxonomy nav_menu. Each link is a post of the type nav_menu_item.
  • A custom item keeps any URL, while add-post links to a page or post by ID and keeps the link updated.

Read WordPress Menus.

Back up the database, delete a post and restore it

Description: Create a post Important post. Make a database backup, delete the post for ever, and then restore the backup. Show the number of posts before the delete, after the delete and after the restore.

Hint:

  • wp db export file.sql and wp db import file.sql.
  • wp post list --format=count gives the number of posts.

Solution

Terminal output

$ wp post create –post_title="Important post" –post_status=publish –porcelain
5
$ wp post list –post_type=post –format=count
2
$ wp db export backup.sql
Success: Exported to 'backup.sql'.
$ wp post delete 5 –force
Success: Deleted post 5.
$ wp post list –post_type=post –format=count
1
$ wp db import backup.sql
Success: Imported from 'backup.sql'.
$ wp post list –post_type=post –format=count
2

How it works

  • The export writes all tables as SQL commands in one file.
  • The import runs the file again and brings the database back to the moment of the backup.
  • Keep the backup file outside the web folder on a real site, and copy wp-content too.

Read Backup and Update.

Build and activate a minimal classic theme

Description: Make a theme folder my-first-theme with a style.css (with the theme name) and an index.php that prints the site name in an h1 tag. Activate it and check the home page.

Hint:

  • The theme name goes in a comment at the top of style.css.
  • Use bloginfo( 'name' ) to print the site name.

Solution

/*
Theme Name: My First Theme
*/
<h1><?php bloginfo( 'name' ); ?></h1>
Terminal output

$ wp theme activate tc-my-first-theme
Success: Switched to 'My First Theme' theme.
$ wp theme list –status=active –fields=name,title
name title
tc-my-first-theme My First Theme
$ curl -s http://example.com/
<h1>Demo Site</h1>

How it works

  • WordPress finds a theme when there is a folder with a style.css which has a Theme Name.
  • index.php is the fallback template, so it is used for every page.

Read Theme Files and Functions.

Show the three latest posts with WP_Query

Description: In the front-page.php of a theme, show a list (ul) of the titles of the three latest posts, each as a link.

Hint:

  • Use new WP_Query( array( 'posts_per_page' => 3 ) ).
  • Call wp_reset_postdata() at the end.

Solution

<?php get_header(); ?>
<?php $latest = new WP_Query( array( 'posts_per_page' => 3 ) ); ?>
<ul class="latest">
<?php while ( $latest->have_posts() ) : $latest->the_post(); ?>
	<li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; wp_reset_postdata(); ?>
</ul>
<?php get_footer(); ?>
Terminal output

$ curl -s http://example.com/ | sed -n '/<ul class="latest">/,/<\/ul>/p'
<ul class="latest">
<li><a href="http://example.com/post-four/">Post four</a></li>
<li><a href="http://example.com/post-three/">Post three</a></li>
<li><a href="http://example.com/post-two/">Post two</a></li>
</ul>

How it works

  • WP_Query gets the newest three posts, because the default order is by date, newest first.
  • the_post() sets the data of each post, so the_title() and the_permalink() work.
  • wp_reset_postdata() gives the global post data back.

Read WP_Query.

Load a stylesheet with wp_enqueue_style and a version

Description: Add a file css/custom.css in your theme and load it with the version 2.0. Check the tag which WordPress prints in the page.

Hint:

  • Use the hook wp_enqueue_scripts and the function wp_enqueue_style().

Solution

<?php
add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_style(
		'tc-custom',
		get_template_directory_uri() . '/css/custom.css',
		array(),
		'2.0'
	);
} );
Terminal output

$ curl -s http://example.com/ | grep "tc-custom" | sed 's/^[ \t]*//'
<link rel='stylesheet' id='tc-custom-css' href='http://example.com/wp-content/themes/tc-theme/css/custom.css?ver=2.0' media='all' />

How it works

  • wp_enqueue_style() adds the stylesheet to the queue, and WordPress prints the link tag in wp_head().
  • The version 2.0 becomes ?ver=2.0. When you change the file, change the version, so the browsers load the new file.

Read Enqueue Scripts and Styles.

Register a sidebar and show a widget in it

Description: Register a sidebar with the ID footer-area, show it in footer.php, add a Search widget to it and check the output.

Hint:

  • Use register_sidebar() on the widgets_init hook and dynamic_sidebar() in the template.
  • wp widget add adds a widget from the command line.

Solution

<?php
add_action( 'widgets_init', function () {
	register_sidebar( array(
		'name'          => 'Footer Area',
		'id'            => 'footer-area',
		'before_widget' => '<div class="widget">',
		'after_widget'  => '</div>',
		'before_title'  => '<h3>',
		'after_title'   => '</h3>',
	) );
} );
Terminal output

$ wp sidebar list –fields=name,id
name id
Footer Area footer-area
Inactive Widgets wp_inactive_widgets
$ wp widget add search footer-area 1 –title="Search the site"
Success: Added widget to sidebar.
$ curl -s http://example.com/ | grep -o '<div class="widget"><h3>[^<]*'
<div class="widget"><h3>Search the site

How it works

  • register_sidebar() creates the widget area, and dynamic_sidebar( 'footer-area' ) prints the widgets which are placed in it.
  • The HTML around the widget and the title comes from the parameters we gave.

Read Register Sidebar.

Show a message only on the front page

Description: Print the text Welcome to our site! at the end of the home page only. The text must not show on posts or pages.

Hint:

  • Use the wp_footer action and the conditional tag is_front_page().

Solution

<?php
add_action( 'wp_footer', function () {
	if ( is_front_page() ) {
		echo '<p class="welcome">Welcome to our site!</p>';
	}
} );
Terminal output

$ curl -s http://example.com/ | grep -o '<p class="welcome">[^<]*</p>'
<p class="welcome">Welcome to our site!</p>
$ curl -s http://example.com/hello-world/ | grep -c 'class="welcome"'
0

How it works

  • The function is attached to wp_footer, which runs on every front page.
  • is_front_page() is true only on the front page, so the message is missing on the post (the count is 0).

Read Conditional Tags.

Create a child theme which changes the footer

Description: Use the demo theme as parent. Create a child theme which has its own footer.php with the text Footer from the child theme. Activate the child and check the output.

Hint:

  • The child style.css needs a Template: line with the folder name of the parent.

Solution

/*
Theme Name: My Child Theme
Template: tc-theme
*/
<footer><p>Footer from the child theme</p></footer>
<?php wp_footer(); ?>
</body>
</html>
Terminal output

$ wp theme activate tc-child
Success: Switched to 'My Child Theme' theme.
$ wp eval 'echo get_template() . " is the parent, " . get_stylesheet() . " is the active theme";'
tc-theme is the parent, tc-child is the active theme
$ curl -s http://example.com/ | grep "<footer>"
<footer><p>Footer from the child theme</p></footer>

How it works

  • Template: tc-theme makes tc-theme the parent. WordPress loads every file from the child first, and from the parent when the file is missing in the child.
  • All other templates (header, index) still come from the parent.

Read WordPress Child Theme.

Write a plugin which adds a text to the footer

Description: Create a plugin called Footer Credit that prints Built with WordPress at the end of every page.

Hint:

  • The plugin needs a header comment with Plugin Name.
  • Use add_action( 'wp_footer', ... ).

Solution

<?php
/**
 * Plugin Name: Footer Credit
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'wp_footer', function () {
	echo '<p class="credit">Built with WordPress</p>';
} );
Terminal output

$ wp plugin activate tc-footer-credit
Plugin 'tc-footer-credit' activated.
Success: Activated 1 of 1 plugins.
$ wp plugin list –name=tc-footer-credit –fields=name,status
name status
tc-footer-credit active
$ curl -s http://example.com/ | grep -o '<p class="credit">[^<]*</p>'
<p class="credit">Built with WordPress</p>

How it works

  • WordPress reads the header comment and shows the plugin in the list.
  • After activation, the file is loaded on every request, and the function waits for the wp_footer action.

Read Create a WordPress Plugin.

Create a [today] shortcode with a date format

Description: Make a shortcode [today] which prints the date in the format d F Y. It should also accept an attribute: [today format="Y-m-d"].

Hint:

  • Use add_shortcode() and shortcode_atts().
  • Return the text, do not echo it. Use wp_date() for the date.

Solution

add_shortcode( 'today', function ( $atts ) {
	$a = shortcode_atts( array( 'format' => 'd F Y' ), $atts, 'today' );
	return esc_html( wp_date( $a['format'] ) );
} );

echo 'Default: ' . ( strlen( do_shortcode( '[today]' ) ) > 0 ? 'works' : 'empty' ) . "\n";
echo 'Format Y-m-d has the right shape: ' . ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', do_shortcode( '[today format="Y-m-d"]' ) ) ? 'yes' : 'no' );
Output

Default: works
Format Y-m-d has the right shape: yes

How it works

  • shortcode_atts() merges the given attributes with the defaults.
  • wp_date() prints the date in the timezone of the site.
  • The output changes every day, so the test checks its shape, not the exact text.

Read Shortcode API.

Register a Movie custom post type

Description: Register a post type movie with the label Movies, an archive at /movies/ and REST support. Add a movie Inception and check its URL.

Hint:

  • Use register_post_type() on the init hook.
  • Flush the rewrite rules once after registering the type.

Solution

<?php
/**
 * Plugin Name: Movies
 */
add_action( 'init', function () {
	register_post_type( 'movie', array(
		'label'        => 'Movies',
		'public'       => true,
		'has_archive'  => true,
		'rewrite'      => array( 'slug' => 'movies' ),
		'show_in_rest' => true,
		'supports'     => array( 'title', 'editor', 'thumbnail' ),
	) );
} );
Terminal output

$ wp plugin activate tc-movies
Plugin 'tc-movies' activated.
Success: Activated 1 of 1 plugins.
$ wp rewrite flush
Success: Rewrite rules flushed.
$ wp post create –post_type=movie –post_title="Inception" –post_status=publish –porcelain
5
$ wp eval 'echo get_permalink( 5 );'
http://example.com/movies/inception/
$ curl -s -o /dev/null -w "archive: %{http_code}\n" http://example.com/movies/
archive: 200
$ curl -s -o /dev/null -w "single: %{http_code}\n" http://example.com/movies/inception/
single: 200

How it works

  • The type is saved in wp_posts with post_type = movie.
  • has_archive gives the list page, and rewrite sets the URL base.
  • Without the flush, the new URLs can show a 404 error.

Read Custom Post Types.

Add a hierarchical Level taxonomy to a Course post type

Description: Register a post type course and a hierarchical taxonomy level. Add the terms Beginner and Advanced. Create a course WordPress Basics in the level Beginner, and list the courses of that level with WP_Query.

Hint:

  • register_taxonomy( 'level', 'course', array( 'hierarchical' => true ) ).
  • Use a tax_query for the list.

Solution

<?php
/**
 * Plugin Name: Courses
 */
add_action( 'init', function () {
	register_post_type( 'course', array( 'label' => 'Courses', 'public' => true, 'show_in_rest' => true ) );
	register_taxonomy( 'level', 'course', array( 'label' => 'Levels', 'hierarchical' => true, 'show_in_rest' => true ) );
} );
Terminal output

$ wp plugin activate tc-courses
Plugin 'tc-courses' activated.
Success: Activated 1 of 1 plugins.
$ wp term create level "Beginner" –slug=beginner –porcelain
2
$ wp term create level "Advanced" –slug=advanced –porcelain
3
$ wp post create –post_type=course –post_title="WordPress Basics" –post_status=publish –porcelain
5
$ wp post create –post_type=course –post_title="Plugin Security" –post_status=publish –porcelain
6
$ wp post term add 5 level beginner
Success: Added term.
$ wp post term add 6 level advanced
Success: Added term.

$q = new WP_Query( array(
	'post_type' => 'course',
	'tax_query' => array( array( 'taxonomy' => 'level', 'field' => 'slug', 'terms' => 'beginner' ) ),
) );
foreach ( $q->posts as $p ) {
	echo $p->post_title . "\n";
}
Output

WordPress Basics

How it works

  • A taxonomy is registered for a post type, and its terms are connected to the posts through wp_term_relationships.
  • The tax_query asks only for the posts which have the term beginner.

Read Custom Taxonomies.

Save a plugin setting with a sanitize callback

Description: Register an option site_slogan in a plugin, which cleans the value with sanitize_text_field. Save the value <b>Learn</b> fast and print what is stored.

Hint:

  • register_setting() with sanitize_callback runs a filter every time update_option() saves the option.

Solution

register_setting( 'my_group', 'site_slogan', array(
	'type'              => 'string',
	'sanitize_callback' => 'sanitize_text_field',
) );

update_option( 'site_slogan', '<b>Learn</b> fast' );
var_dump( get_option( 'site_slogan' ) );
Output

string(10) "Learn fast"

How it works

  • register_setting() connects the sanitize function to the option name.
  • The tags are removed before the value goes to the database, so the stored value is plain text.
  • In a real plugin you call register_setting() on the admin_init hook.

Read Plugin Settings Page.

Create a custom table and insert a row with $wpdb

Description: Create a table wp_tc_visits with the columns id, page and hits. Insert the row (home, 10), and read it back with a prepared query.

Hint:

  • Use $wpdb->prefix, dbDelta(), $wpdb->insert() and $wpdb->prepare().

Solution

global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';

$table   = $wpdb->prefix . 'tc_visits';
$charset = $wpdb->get_charset_collate();

dbDelta( "CREATE TABLE $table (
	id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
	page varchar(100) NOT NULL,
	hits int(11) NOT NULL DEFAULT 0,
	PRIMARY KEY  (id)
) $charset;" );

$wpdb->insert( $table, array( 'page' => 'home', 'hits' => 10 ), array( '%s', '%d' ) );

$row = $wpdb->get_row( $wpdb->prepare( "SELECT page, hits FROM $table WHERE page = %s", 'home' ) );
echo $row->page . ' => ' . $row->hits;
Output

home => 10

How it works

  • dbDelta() creates the table (and it would update it if it existed).
  • insert() writes the row and prepares the values with the formats %s and %d.
  • prepare() protects the select query from SQL injection.

Read WordPress $wpdb.