Experienced WordPress Interview Questions
How to limit WordPress post content length?
Use wp_trim_words() to cut a text to a number of words, and the excerpt_length filter to change the length of automatic excerpts.
$content = 'WordPress is a free and open source CMS. It runs a large part of the web and has thousands of themes and plugins for every kind of site.';
echo wp_trim_words( $content, 10, '...' ) . "\n";
echo wp_html_excerpt( $content, 40, '...' ) . "\n";
echo mb_strimwidth( $content, 0, 40, '...' );
WordPress is a free and open source CMS. It runs…
WordPress is a free and open source CMS….
WordPress is a free and open source C…
wp_trim_words( $text, $num_words, $more ): counts words, and removes the HTML tags.wp_html_excerpt( $str, $count, $more ): counts characters, and removes the HTML tags.
To change the length of the automatic excerpt (used by the_excerpt()), add a filter in functions.php:
add_filter( 'excerpt_length', function () {
return 20; // words
}, 999 );
add_filter( 'excerpt_more', function () {
return '...';
} );
Inside the loop you can use it with the content: echo wp_trim_words( get_the_content(), 30 );. The default excerpt is 55 words.
What is the WordPress default feed cache recreation time? How to change it?
When WordPress reads a remote feed with fetch_feed() (for example in the dashboard news widget), it saves the result in a transient so it does not ask the other site every time. The default cache time is 12 hours, which is 43200 seconds.
Change it with the wp_feed_cache_transient_lifetime filter. The filter gets the time in seconds, and the feed URL:
$default = apply_filters( 'wp_feed_cache_transient_lifetime', 12 * HOUR_IN_SECONDS, 'https://example.com/feed/' );
echo "Default: $default seconds\n";
add_filter( 'wp_feed_cache_transient_lifetime', function ( $seconds, $url ) {
return 30 * MINUTE_IN_SECONDS; // cache for 30 minutes
}, 10, 2 );
$new = apply_filters( 'wp_feed_cache_transient_lifetime', 12 * HOUR_IN_SECONDS, 'https://example.com/feed/' );
echo "After the filter: $new seconds";
Default: 43200 seconds
After the filter: 1800 seconds
A shorter time shows new feed items faster, but uses more requests to the remote server. This is not the same as the feed of your own site, which is made fresh on every request.
How to create a WordPress global options page?
Use the Settings API: add_options_page() for the page, register_setting() for the option, and add_settings_field() for the fields. This is the shortest complete example:
add_action( 'admin_menu', function () {
add_options_page( 'My Options', 'My Options', 'manage_options', 'my-options', 'my_options_page' );
} );
add_action( 'admin_init', function () {
register_setting( 'my_group', 'my_company', array( 'sanitize_callback' => 'sanitize_text_field' ) );
add_settings_section( 'my_section', 'Company', '__return_false', 'my-options' );
add_settings_field( 'my_company', 'Company name', function () {
echo '<input type="text" name="my_company" value="' . esc_attr( get_option( 'my_company' ) ) . '">';
}, 'my-options', 'my_section' );
} );
function my_options_page() { ?>
<div class="wrap">
<h1>My Options</h1>
<form action="options.php" method="post">
<?php settings_fields( 'my_group' ); do_settings_sections( 'my-options' ); submit_button(); ?>
</form>
</div>
<?php }
Read the value anywhere with get_option( 'my_company' ). The sanitize callback cleans the value when it is saved:
register_setting( 'my_group', 'my_company', array( 'sanitize_callback' => 'sanitize_text_field' ) );
update_option( 'my_company', '<b>Acme</b> <script>x()</script>Ltd' );
echo get_option( 'my_company' );
Acme Ltd
Full steps with a tested plugin: WordPress Plugin Settings Page.
How to create a WordPress plugin from scratch?
Four steps:
- Make a folder in
wp-content/plugins, likemy-plugin. - Add a PHP file with the same name. Write the header comment with at least
Plugin Name. - Write your code with hooks (actions and filters), a unique prefix and an
ABSPATHcheck. - Activate it from the Plugins screen (or
wp plugin activate my-plugin).
<?php
/**
* Plugin Name: TC Hello Plugin
* Description: Adds a small text in the footer.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_footer', function () {
echo '<p class="tchp">Made with WordPress</p>';
} );
$ wp plugin activate tc-hello-plugin
Plugin 'tc-hello-plugin' activated.
Success: Activated 1 of 1 plugins.
$ curl -s http://example.com/ | grep -o '<p class="tchp">[^<]*</p>'
<p class="tchp">Made with WordPress</p>
A full tested example with activation and uninstall is in Create a WordPress Plugin.
How to increase maximum execution time in PHP and WordPress?
A long job (an import, a big backup or a slow plugin) can stop with Maximum execution time exceeded. The limit is set by the PHP setting max_execution_time (in seconds). Ways to raise it:
- php.ini:
max_execution_time = 300 - .htaccess (Apache with mod_php):
php_value max_execution_time 300 - In code: call
set_time_limit( 300 );orini_set( 'max_execution_time', '300' );before the long job. - Your host panel: many hosts have a PHP settings page.
Here we check the value on a real page, before and after calling set_time_limit() from a small must use plugin:
<?php
add_action( 'wp_footer', function () {
echo '<!-- before: ' . ini_get( 'max_execution_time' ) . ' -->';
set_time_limit( 300 );
echo '<!-- after: ' . ini_get( 'max_execution_time' ) . ' -->';
} );
$ curl -s http://example.com/ | grep -o "<!– \(before\|after\): [0-9]* –>"
<!– before: 30 –>
<!– after: 300 –>
WP-CLI scripts have no time limit by default, which is why long jobs are often done with it. Some hosts block set_time_limit(), and a web server (Nginx or a proxy) can have its own timeout too. Do not set it too high on a live site.
What menu positions and order are available in WordPress?
When you add a top level menu with add_menu_page(), the last parameter is the position. A lower number shows higher in the menu. The default positions are:
| Position | Menu item |
|---|---|
| 2 | Dashboard |
| 4 | Separator |
| 5 | Posts |
| 10 | Media |
| 15 | Links (hidden on new sites) |
| 20 | Pages |
| 25 | Comments |
| 59 | Separator |
| 60 | Appearance |
| 65 | Plugins |
| 70 | Users (or Profile) |
| 75 | Tools |
| 80 | Settings |
| 99 | Separator |
You can see them in the WordPress source file wp-admin/menu.php:
$ grep -n "^\$menu\[" wp-admin/menu.php | cut -c1-60
29:$menu[2] = array( __( 'Dashboard' ), 'read', 'index.php',
69:$menu[4] = array( '', 'read', 'separator1', '', 'wp-menu-
73:$menu[10] = array( __( 'Media' ), 'upload_files', 'upload
89:$menu[15] = array( __( 'Links' ), 'manage_links', 'link-m
205:$menu[59] = array( '', 'read', 'separator2', '', 'wp-men
209:$menu[60] = array( __( 'Appearance' ), $appearance_capab
323:$menu[65] = array( sprintf( __( 'Plugins %s' ), $count )
390:$menu[75] = array( __( 'Tools' ), 'e
405:$menu[80] = array( __( 'Se
417:$menu[99] = array( '', 'read', 'separator-last', '', 'wp
add_action( 'admin_menu', function () {
add_menu_page( 'Reports', 'Reports', 'manage_options', 'my-reports', 'my_reports_page', 'dashicons-chart-bar', 26 );
} );
With position 26, the menu shows after Comments (25). Do not use the same number as another item: pick a decimal like 26.5 (as a string) to avoid an overwrite.
Which Gutenberg blocks support wide and full width alignment?
Wide and full width alignment make a block go outside the normal content width. A block supports it through its supports.align setting. We can ask the block registry of WordPress 7.1 for the real list:
$wide_full = array();
foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $name => $block ) {
$align = $block->supports['align'] ?? null;
if ( $align === true ) {
$wide_full[] = $name; // true means all alignments
} elseif ( is_array( $align ) && in_array( 'wide', $align, true ) && in_array( 'full', $align, true ) ) {
$wide_full[] = $name;
}
}
sort( $wide_full );
echo count( $wide_full ) . " blocks support both wide and full:\n" . wordwrap( implode( ', ', $wide_full ), 90, "\n" );
53 blocks support both wide and full:
core/accordion, core/archives, core/audio, core/avatar, core/breadcrumbs, core/buttons,
core/calendar, core/categories, core/columns, core/comment-template, core/comments,
core/comments-pagination, core/comments-title, core/cover, core/details, core/embed,
core/file, core/gallery, core/group, core/heading, core/image, core/latest-comments,
core/latest-posts, core/media-text, core/navigation, core/paragraph, core/playlist,
core/post-comments, core/post-content, core/post-featured-image, core/post-template,
core/post-title, core/pullquote, core/query, core/query-no-results, core/query-pagination,
core/query-title, core/query-total, core/quote, core/rss, core/separator, core/site-logo,
core/site-tagline, core/site-title, core/table, core/tabs, core/tag-cloud,
core/template-part, core/term-description, core/term-name, core/term-template,
core/terms-query, core/video
The list includes Image, Gallery, Cover, Group, Columns, Heading, Paragraph, Table, Video, Quote, Query Loop and the Navigation block. The list changes in new versions of WordPress, so use the code above to see the list of your own site.
The theme must allow it
For a block theme wide and full alignment are on by default. A classic theme must turn it on:
add_theme_support( 'align-wide' );
The width of the “wide” area comes from layout.wideSize in theme.json. See Block Themes and theme.json.
What is the template hierarchy in WordPress?
When a page is requested, WordPress finds out its type (single post, page, category and so on) and looks for a template file in a fixed order, from the most specific name to the most general one. The last choice is always index.php.
| Page | Order of files |
|---|---|
| Single post | single-{post-type}-{slug}.php, single-{post-type}.php, single.php, singular.php, index.php |
| Page | page-{slug}.php, page-{id}.php, page.php, singular.php, index.php |
| Category | category-{slug}.php, category-{id}.php, category.php, archive.php, index.php |
| Front page | front-page.php, then home.php or page.php, index.php |
| 404 | 404.php, index.php |
You can test it with a theme where each file prints its own name. We did exactly that in WordPress Template Hierarchy. In block themes the same logic is used, but with files like templates/single.html.
What is the difference between WP_Query, get_posts() and query_posts()?
| Method | What it does | Use? |
|---|---|---|
| WP_Query | Class. Makes a new query object, and gives you the loop, pagination and many properties | Yes, for custom loops |
| get_posts() | Function. Returns a simple array of posts. No loop setup, no pagination | Yes, for simple lists |
| query_posts() | Replaces the main query, and breaks pagination and conditional tags | No, never |
| pre_get_posts | Hook. Changes the main query before it runs | Yes, for the main query |
// get_posts(): an array of post objects, no loop setup
$posts = get_posts( array( 'posts_per_page' => 2, 'post_type' => 'post' ) );
foreach ( $posts as $p ) {
echo $p->ID . ' ' . $p->post_title . "\n";
}
// WP_Query: a loop with the helper methods
$q = new WP_Query( array( 'posts_per_page' => 2 ) );
echo 'Total found: ' . $q->found_posts . ', pages: ' . $q->max_num_pages;
6 Third post
5 Second post
Total found: 3, pages: 2
get_posts() also sets suppress_filters to true by default, so some plugin filters do not run. To change the main query use pre_get_posts, see WP_Query.
What is the pre_get_posts hook?
pre_get_posts gives you the WP_Query object before the database is asked. You can change its arguments with $query->set(). Always check that it is the main query, and not in the admin, or you will change other queries by mistake.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
$query->set( 'posts_per_page', 2 );
}
} );
$ curl -s http://example.com/ | grep -o 'target="_self" >[^<]*' | sed 's/.*>//'
Post C
Post B
The home page lists only two posts now (without the hook it shows ten). Use it also for custom post types in archives, search results or to change the order.
What is the difference between actions and filters in WordPress?
Both are hooks, and both are added with a callback function.
| Point | Action | Filter |
|---|---|---|
| Add | add_action() | add_filter() |
| Run | do_action() | apply_filters() |
| Purpose | Do something (print, save, send) | Change a value |
| Return | Nothing | The changed value, always |
add_action( 'tc_before', function () { echo "action: runs, returns nothing\n"; } );
do_action( 'tc_before' );
add_filter( 'tc_title', function ( $title ) { return strtoupper( $title ); } );
echo 'filter: ' . apply_filters( 'tc_title', 'hello' );
action: runs, returns nothing
filter: HELLO
The most common mistake in a filter is to forget return, which makes the value empty. Read Hooks: Actions and Filters.
What is the difference between sanitizing, validating and escaping in WordPress?
| Step | When | Example |
|---|---|---|
| Validate | Check, reject if wrong | is_email( $email ) |
| Sanitize | Clean the input before saving | sanitize_text_field( $_POST['name'] ) |
| Escape | Make safe when printing | esc_html( $name ), esc_url( $url ), esc_attr( $value ) |
$input = '<script>alert(1)</script>Riya';
echo 'Sanitize (save): ' . sanitize_text_field( $input ) . "\n";
echo 'Escape (print): ' . esc_html( $input ) . "\n";
echo 'Validate: ' . var_export( is_email( 'riya@example' ), true );
Sanitize (save): Riya
Escape (print): <script>alert(1)</script>Riya
Validate: false
Remember: sanitize early, escape late. Details are in Data Validation, Sanitizing and Escaping.
What is a nonce in WordPress?
A WordPress nonce (“number used once”) is a token which proves that a request came from your own site, for a specific user and action. It stops CSRF attacks, where another site tricks a logged in user into sending a request. A WordPress nonce is valid for a limited time (12 to 24 hours), it is not truly used only once.
wp_set_current_user( 1 );
$nonce = wp_create_nonce( 'delete_item_5' );
echo 'Right action: ' . var_export( (bool) wp_verify_nonce( $nonce, 'delete_item_5' ), true ) . "\n";
echo 'Other action: ' . var_export( (bool) wp_verify_nonce( $nonce, 'delete_item_6' ), true );
Right action: true
Other action: false
- Forms:
wp_nonce_field( 'action', 'name' )and check withcheck_admin_referer(). - Links:
wp_nonce_url( $url, 'action' ). - AJAX and REST: send it in the
X-WP-Nonceheader (created withwp_create_nonce( 'wp_rest' )).
A nonce is not a permission check. Also use current_user_can().
What is the WordPress REST API?
The REST API lets other applications talk to WordPress with HTTP requests, and the answer is JSON. The base is /wp-json/. It is used by the block editor, mobile apps and headless front ends (React, Vue).
$ curl -s "http://example.com/wp-json/wp/v2/posts?per_page=1&_fields=id,slug,title" | jq -c .
[{"id":1,"slug":"hello-world","title":{"rendered":"Hello world!"}}]
GETreads,POSTcreates or updates,DELETEremoves.- Public content can be read without a login. Writing needs authentication: cookie with nonce, or application passwords.
- You can add your own routes with
register_rest_route().
See REST API Introduction and Authentication.
How to create a custom REST API endpoint in WordPress?
Use register_rest_route() inside the rest_api_init action. Always give a permission_callback.
<?php
// A small function, because PHP 8 does not allow is_numeric() directly as the callback:
// WordPress passes 3 values and is_numeric() accepts only one.
function tc_is_number( $value ) {
return is_numeric( $value );
}
add_action( 'rest_api_init', function () {
register_rest_route( 'tc/v1', '/sum', array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'args' => array(
'a' => array( 'required' => true, 'validate_callback' => 'tc_is_number' ),
'b' => array( 'required' => true, 'validate_callback' => 'tc_is_number' ),
),
'callback' => function ( WP_REST_Request $request ) {
return array( 'sum' => $request['a'] + $request['b'] );
},
) );
} );
$ curl -s "http://example.com/wp-json/tc/v1/sum?a=5&b=7"
{"sum":12}
$ curl -s "http://example.com/wp-json/tc/v1/sum?a=5&b=x" | jq -c '{code, message}'
{"code":"rest_invalid_param","message":"Invalid parameter(s): b"}
The route has a namespace (tc/v1), validated parameters and a public permission. Notice the helper function tc_is_number(): with PHP 8, a PHP built in function like is_numeric cannot be used directly as the callback, because WordPress sends three values to it, and the built in function accepts only one. This is a very common cause of the critical error in REST code. Read the full chapter: Custom REST Endpoint.
What are transients in WordPress?
Transients are a simple way to cache data for a limited time, like the result of a slow query or a remote API call. You save a value with a name and an expiry time. After that time the value is gone and you make it again.
$key = 'tc_slow_data';
$data = get_transient( $key );
if ( false === $data ) {
echo "Cache miss: building the data\n";
$data = array( 'built_at' => 'now', 'items' => range( 1, 3 ) );
set_transient( $key, $data, HOUR_IN_SECONDS );
} else {
echo "Cache hit\n";
}
$again = get_transient( $key );
echo 'Second read: ' . ( $again ? 'cache hit, items: ' . implode( ',', $again['items'] ) : 'miss' ) . "\n";
delete_transient( $key );
var_dump( get_transient( $key ) );
Cache miss: building the data
Second read: cache hit, items: 1,2,3
bool(false)
- By default transients are saved in the
wp_optionstable (with a_transient_prefix). When a persistent object cache (Redis or Memcached) is installed, they go there instead. - Always check
false === $data, because a transient can be missing at any time. - The expiry is a maximum. The cache can be cleared earlier.
- Do not save sensitive data in transients.
For per request caching use wp_cache_get() and wp_cache_set() (the object cache).
What is WP-Cron in WordPress? How is it different from a real cron job?
WP-Cron is the scheduler of WordPress. It does not run all the time like a server cron. It checks for due tasks when a page is loaded (through wp-cron.php), so a task can be late if there are no visitors.
$ wp cron event list –fields=hook,recurrence
hook recurrence
recovery_mode_clean_expired_keys 1 day
wp_privacy_delete_old_export_files 1 hour
wp_privacy_personal_data_cleanup_requests 1 day
wp_version_check 12 hours
wp_update_plugins 12 hours
wp_update_themes 12 hours
wp_site_health_scheduled_check 1 week
Schedule your own task with wp_schedule_event() and attach a function to its hook:
add_action( 'tc_daily_task', function () {
update_option( 'tc_last_run', 'task ran' );
} );
if ( ! wp_next_scheduled( 'tc_daily_task' ) ) {
wp_schedule_event( time() + 3600, 'daily', 'tc_daily_task' );
}
echo 'Scheduled: ' . ( wp_next_scheduled( 'tc_daily_task' ) ? 'yes' : 'no' ) . "\n";
do_action( 'tc_daily_task' ); // run it now, to test
echo get_option( 'tc_last_run' );
Scheduled: yes
task ran
Use a real cron for big sites
Turn off the visit based trigger, and call wp-cron.php from the server every few minutes:
// wp-config.php
define( 'DISABLE_WP_CRON', true );
*/5 * * * * cd /var/www/html && wp cron event run --due-now >/dev/null 2>&1
Remember to remove the scheduled event when your plugin is deactivated, with wp_clear_scheduled_hook().
What is a block theme and the theme.json file?
A block theme is a theme whose templates are HTML files made of blocks (in templates/ and parts/). It has no PHP templates. The design settings (colors, fonts, spacing, layout) are in theme.json, and users can change them in the Site Editor.
$ wp eval 'echo wp_is_block_theme() ? "block theme" : "classic theme";'
block theme
$ ls wp-content/themes/twentytwentyfive
assets
contributing.txt
functions.php
package-lock.json
package.json
parts
patterns
readme.txt
screenshot.png
style.css
style.min.css
styles
templates
theme.json
- settings in theme.json: what the editor offers (palette, font sizes, layout width).
- styles: the real design values, saved as CSS variables like
--wp--preset--color--primary. - Classic themes use PHP templates and the Customizer.
How to prevent SQL injection in WordPress?
SQL injection happens when user input becomes a part of a SQL command. To prevent it in WordPress:
- Use the built in functions first:
WP_Query,get_posts(),get_option(),update_post_meta(). They are safe. - When you write SQL with
$wpdb, always use$wpdb->prepare()with%s,%dand%f. - Use
$wpdb->insert(),update()anddelete(), which prepare the values for you. - For a list inside
IN (...), build the placeholders yourself.
global $wpdb;
$bad = "x' OR '1'='1";
echo "Unsafe: SELECT ID FROM {$wpdb->posts} WHERE post_title = '$bad'\n";
echo 'Prepared: ' . $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s", $bad );
Unsafe: SELECT ID FROM wp_posts WHERE post_title = 'x' OR '1'='1'
Prepared: SELECT ID FROM wp_posts WHERE post_title = 'x\' OR \'1\'=\'1'
In the unsafe query the quote ends the text, and the rest OR '1'='1' becomes SQL. In the prepared one, the quote is escaped, so the whole input is only a value. See WordPress $wpdb.
How to speed up a WordPress site?
Check the speed first with PageSpeed Insights or GTmetrix, find the slow part, and then fix it. The most useful steps are:
- Good hosting and a recent PHP version (PHP 8.3 or newer is recommended by WordPress.org).
- Page caching with a plugin (LiteSpeed Cache, WP Super Cache) or the cache of your host.
- Optimize the images: resize them, use WebP or AVIF, and lazy loading (WordPress adds
loading="lazy"by itself). - A light theme and fewer, better plugins. Remove what you do not use.
- Use a CDN for static files.
- Object cache (Redis or Memcached) for sites with a lot of database work.
- Clean the database: remove old revisions, spam comments and expired transients. Limit revisions with
WP_POST_REVISIONS. - Load scripts only where needed and keep the number of requests small.
$ wp plugin list –status=active –format=count
0
$ wp transient delete –expired
Success: No expired transients found.
$ wp post list –post_type=revision –format=count
0
Measure again after every change, so you know what really helped.