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 createwith--post_type=pagefor the page. - The flag
--porcelainprints only the new ID. wp post list --post_type=pagelists the pages.
Solution
$ 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 createinserts a row in thewp_poststable. Thepost_typedecides 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=IDmakes a child category.wp post createhas the flags--post_categoryand--tags_input.
Solution
$ 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
blognameandblogdescription. - In the dashboard it is Settings > General.
Solution
$ 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 – Learn WordPress step by step</title>
How it works
- Settings are rows in the
wp_optionstable.wp option updatechanges 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_frontandpage_for_posts.
Solution
$ 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_frontispagewhen 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 createwith--role. - The PHP function
user_can( $user_id, $capability )checks a capability.
Solution
$ 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, whilecurrent_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
$ 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-postandwp menu item add-custom.
Solution
$ 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 typenav_menu_item. - A custom item keeps any URL, while
add-postlinks 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.sqlandwp db import file.sql.wp post list --format=countgives the number of posts.
Solution
$ 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-contenttoo.
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>
$ 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.csswhich has aTheme Name. index.phpis the fallback template, so it is used for every page.
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(); ?>
$ 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_Querygets the newest three posts, because the default order is by date, newest first.the_post()sets the data of each post, sothe_title()andthe_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_scriptsand the functionwp_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'
);
} );
$ 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 thelinktag inwp_head().- The version
2.0becomes?ver=2.0. When you change the file, change the version, so the browsers load the new file.
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 thewidgets_inithook anddynamic_sidebar()in the template. wp widget addadds 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>',
) );
} );
$ 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, anddynamic_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_footeraction and the conditional tagis_front_page().
Solution
<?php
add_action( 'wp_footer', function () {
if ( is_front_page() ) {
echo '<p class="welcome">Welcome to our site!</p>';
}
} );
$ 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.cssneeds aTemplate: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>
$ 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-thememakestc-themethe 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>';
} );
$ 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_footeraction.
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()andshortcode_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' );
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 theinithook. - 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' ),
) );
} );
$ 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_postswithpost_type = movie. has_archivegives the list page, andrewritesets 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_queryfor 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 ) );
} );
$ 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";
}
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_queryasks 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()withsanitize_callbackruns a filter every timeupdate_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' ) );
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 theadmin_inithook.
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;
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%sand%d.prepare()protects the select query from SQL injection.
Read WordPress $wpdb.