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
A classic widget is a PHP class that extends WP_Widget, with methods to display the widget, show its settings form in the admin and save those settings — and it can be placed in any widget area the theme provides.
<?php
class GDC_Books_Recent_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'gdc_books_recent', // Base ID.
__( 'Recent Books', 'gdc-books' ), // Name in the admin.
array( 'description' => __( 'Your latest books.', 'gdc-books' ) )
);
}
/**
* Front-end output.
*/
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Books', 'gdc-books' );
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
$books = get_posts(
array(
'post_type' => 'gdc_book',
'numberposts' => $count,
)
);
if ( ! $books ) {
return;
}
echo $args['before_widget']; // Markup from the theme's register_sidebar().
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title, $instance, $this->id_base ) ) . $args['after_title'];
echo '<ul>';
foreach ( $books as $book ) {
printf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink( $book ) ), esc_html( get_the_title( $book ) ) );
}
echo '</ul>';
echo $args['after_widget'];
}
/**
* Admin settings form.
*/
public function form( $instance ) {
$title = $instance['title'] ?? '';
$count = $instance['count'] ?? 5;
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'gdc-books' ); ?></label>
<input class="widefat" type="text"
id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
value="<?php echo esc_attr( $title ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of books:', 'gdc-books' ); ?></label>
<input class="tiny-text" type="number" min="1" max="20"
id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
value="<?php echo esc_attr( $count ); ?>">
</p>
<?php
}
/**
* Sanitises settings before they are saved.
*/
public function update( $new_instance, $old_instance ) {
return array(
'title' => sanitize_text_field( $new_instance['title'] ?? '' ),
'count' => min( 20, max( 1, absint( $new_instance['count'] ?? 5 ) ) ),
);
}
}
add_action( 'widgets_init', function () {
register_widget( 'GDC_Books_Recent_Widget' );
} );| Method | Runs | Job |
|---|---|---|
__construct() | Once | Sets the ID, name and description. |
widget() | On the front end | Prints the widget, wrapped in the theme's before_widget / after_widget. |
form() | In the admin | Prints the settings fields. get_field_id() and get_field_name() keep several copies of the widget from clashing. |
update() | On save | Sanitises and returns the settings to store. |
$args['before_widget'] and the title wrappers come from the theme's register_sidebar() call — that is how one widget looks right in every theme. They are trusted markup and are printed as-is.
Since WordPress 5.8 the Widgets screen uses blocks. Classic widgets still work — they appear inside a "Legacy Widget" block — and remain the right tool for plugins supporting classic themes. For anything new and visual, a block (next lesson) is the modern equivalent and works everywhere: posts, pages, widget areas and the Site Editor.
999 as the count and confirm update() caps it at 20.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.