Skip to content

Proposal: Add On This Day Widget#11630

Open
alshakero wants to merge 59 commits into
WordPress:trunkfrom
alshakero:add/on-this-day-widget
Open

Proposal: Add On This Day Widget#11630
alshakero wants to merge 59 commits into
WordPress:trunkfrom
alshakero:add/on-this-day-widget

Conversation

@alshakero

@alshakero alshakero commented Apr 22, 2026

Copy link
Copy Markdown

Summary

Adds a new On This Day dashboard widget to WordPress core that surfaces the current user's posts published on today's month and day in previous years, so returning authors see a friendly nudge of what they wrote one, five, or ten years ago.

The widget is implemented as a first-class core feature, following the same pattern as Site Health.

User-facing behavior

  • Widget title reads On This Day · <Month Day> and appears in the dashboard grid for users with edit_posts.
  • Each year is shown with a year badge (e.g. 2023 · 3 yrs) and the posts published that day as cards with excerpt, time, categories, and Edit/View links.
  • Draft and private posts are included for the author and visually distinguished; public posts link to the edit screen with a View link to the permalink.
  • Empty state messaging encourages the author when today has no historical posts.

Screenshots

image

Testing

  • Checkout this PR locally and run npm run env:start, then npm run env:install.
  • Go to Dashboard (admin:password), you should see the widget.
  • Insepect its empty state.
  • Import this file to create backdated posts: filexml
  • Use the widget.

Trac ticket: https://core.trac.wordpress.org/ticket/65116#ticket

Use of AI Tools

AI assistance: Yes
Tool(s): Cursor
Model(s): Claude Opus 4.7 + Codex 5.5
Used for: The code is ~50% written with AI, but I guided it every step of the way and reviewed every line.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@alshakero alshakero marked this pull request as ready for review April 22, 2026 22:19
@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @escapemanuele.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props alshakero, jeherve, apermo, dmsnell, jonsurrell, jorbin, peterwilsoncc, wildworks, joedolson, talldanwp, kellychoffman, simison, joen, retrofox, matt, sabernhardt, annezazu.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@escapemanuele

Copy link
Copy Markdown
image

Nice, nice! I must have some CSS issue here as I do not see the dates on the left.

@alshakero

alshakero commented Apr 23, 2026

Copy link
Copy Markdown
Author

Nice, nice! I must have some CSS issue here as I do not see the dates on the left.

It looks like the Playground is running an outdated build. I rebased to rebuilt. Hopefully that will fix it.

Edit: It's a playground issue, I updated the testing steps.

@escapemanuele

Copy link
Copy Markdown

Tested locally and it loads just fine!

@alshakero

Copy link
Copy Markdown
Author

Redesigned following this.

image

@jeherve jeherve left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been poking at the query logic and comparing against my own little plugin. I thought I'd mention a few patterns I picked up from feedback from users.

Use date_query instead of raw SQL

Right now filter_posts_where() uses a posts_where filter with hand-rolled MONTH()/DAY()/YEAR() comparisons. It works, but WP_Query already supports this natively through date_query; it handles the escaping, uses the documented API surface, and is friendlier to anyone who later wants to extend the behavior. class-query.php from the plugin is a reasonable reference.

Consider widening the window beyond the exact day

Of note, most "memory" products — Google Photos, Apple's On This Day, Facebook Memories — don't restrict to the exact calendar day; they widen the window to nearby dates so something shows up even on slow days. The plugin started the same way this PR does, and I eventually moved to a week-long window by default, with exact-day matching as an opt-in.

A couple of reasons this matters:

  • Feb 29. With exact matching, leap-day posts only surface once every four years, and on Feb 29 in a non-leap year… well, that day doesn't exist, so the widget is awkwardly blank.
  • Sparse posters. Someone who publishes weekly but not daily will see the empty state on most days of the year; a small window (±3 days?) turns that into a useful recap instead.

I'm wondering if we could default to a modest window and leave exact-match behind a filter for folks who really want it. Closer to how the genre works in the wild.

<time datetime="…"> needs a timezone

Small thing on the post meta row:

$time_iso = get_the_time( 'Y-m-d H:i', $post );
// ...
<time datetime="<?php echo esc_attr( $time_iso ); ?>">

get_the_time() returns the post time in the site's timezone, but the emitted string has no offset or Z. Per the HTML spec that's a "local date and time" without context, so screen readers and timezone-aware tooling interpret it as the user's local time rather than the site's. Either drop to Y-m-d (a plain date is a valid datetime value), or switch to get_the_time( 'c', $post ), which gives you ISO 8601 with an offset attached.

*
* @since 7.1.0
*/
#[AllowDynamicProperties]

@apermo apermo Apr 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is allow dynamic properties needed? As it is new code, I would refrain from adding this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. I was undecided about this attribute tbqh. But I was mimicking Site Health and decided to keep it for consistency. But it's not needed for my class.

I removed it.

@alshakero

alshakero commented Apr 25, 2026

Copy link
Copy Markdown
Author

Thanks for the amazing feedback, @jeherve!

Use date_query instead of raw SQL

Done.

Consider widening the window beyond the exact day

I added a minimal slider to keep the noise down the allows adjusting the range from 1 to 7 days.

needs a timezone

Fixed.

@dmsnell dmsnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of the calls to translation which directly output (e.g. via printf()) need to be escaped so they don’t break the page. this includes calls to _e().

happy to give another round here. looks like a nice widget

'on-this-day',
sprintf(
'#dashboard_on_this_day{--otd-today:%s;}',
wp_json_encode( self::get_window_label( self::get_window_days() ) )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what the intention here is with JSON encoding, but this is CSS, which does not understand JSON.

if you are looking to escape a CSS string it would be best to follow CSS language rules otherwise this will open up unexpected corruption.

calling @sirreal on this one, but I would imagine that this would be preferable to json_encode()

$escaped_label = strtr( self::get_window_label( ... ), '"', '\"' );

there are other details, like forbidding newline characters or invalid UTF-8 in the string, but JSON encoding has its own list of corrupting circumstances

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a common repurposing of json_encode to wrap loose strings with quotes and escape any occurring quotes if any. It also escapes new lines and a few other baddies. Frankly I think it's probably safer than making my own function. But GPT-5.5 obliged and created a helper.

Happy to settle for either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you, and I’ll defer to @sirreal who can speak much more eloquently on this than I can.

having a wrapper at least leaves more intention in the code which can be later cleaned up, but using JSON serialization hides that intention. there is work to add string escaping in Core, which will be preferable here anyway once it arrives.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I side stepped this by avoiding CSS props in favor of HTML attributes.

Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
@alshakero alshakero force-pushed the add/on-this-day-widget branch from 2664880 to b4c40ed Compare June 18, 2026 20:33
@alshakero

Copy link
Copy Markdown
Author

Can we not show anything if there's nothing on this day in the past?

It might be tricky to show nothing at all. Because the widgets are customizable by the user and dynamically hiding them may mess up the ordering. This is what Years Ago Today shows.

image

I have used this plugin for a while and prefer it as a starting point to the above: wordpress.org/plugins/years-ago-today

Happy to adopt that, the UI is simpler and more performant (no carousel CSS and JS). But I'm curious which part would you like me to bring over?

  • The window size? Keep it to a day instead of a week?
  • The UI design?

@sabernhardt

Copy link
Copy Markdown

Can we not show anything if there's nothing on this day in the past?

A new, separate widget would not be very valuable on a site with zero published posts, and it would be inappropriate for sites that never have any published posts.

The existing Activity widget has a 'No activity yet!' fallback when there are no future or published posts and no recent comments. I think the Published On This Day list could fit inside that widget, possibly with the same styling and similar links. A made a quick proof of concept on my fork.

@peterwilsoncc peterwilsoncc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a few more notes inline, thanks for following up my earlier review.

Comment thread src/wp-admin/includes/class-wp-dashboard-widget-on-this-day.php Outdated
endif;
?>
<ul class="wp-on-this-day-timeline<?php echo $is_carousel ? ' wp-on-this-day-is-carousel' : ''; ?>">
<?php foreach ( $posts as $index => $post ) : ?>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the loop.

}

$chunk = $processor->get_modifiable_text();
$parts[] = $chunk;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Dennis that this would be clearer with concatenation.

$clauses = array();

for ( $offset = -self::WINDOW_BEFORE_DAYS; $offset <= self::WINDOW_AFTER_DAYS; $offset++ ) {
$day_date = $date->modify( ( $offset >= 0 ? '+' : '' ) . $offset . ' days' );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a comment @jeherve use leap days as an example that needs to be considered. As written this code will only display leap days in a leap year as the offset is based on the current year, ie in 2026 the combination 2, 29 won't be included.

Maybe you can add it in for the combo 2,28 if $date->format( 'L' ) == 0 but Jeremy might have a better solution.

Comment thread src/wp-admin/includes/class-wp-dashboard-widget-on-this-day.php Outdated
Comment thread src/wp-admin/includes/class-wp-dashboard-widget-on-this-day.php Outdated
@alshakero

alshakero commented Jun 29, 2026

Copy link
Copy Markdown
Author

@m

Can we not show anything if there's nothing on this day in the past?

Yes. Apparently the grid reflows nicely when an item is removed. We now don’t render anything for empty days.

I have used this plugin for a while and prefer it as a starting point to the above: years-ago-today

Yes, I took a lot of inspiration from that widget. Here’s how it’s looking now:

image

I have two questions:

  1. This plugin sends daily emails too, do we want that too?
  2. The plugin adds a Gutenberg block

@peterwilsoncc thank you so much for your generous feedback. I addressed all the parts that apply to the new design.

@dmsnell dmsnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alshakero thanks for your patience with my review, as I was delayed in getting back to this.

I left some comments in there, but what else do you think this needs before it’s ready? is there any help you want or think is necessary?

* @param int $user_id Current user ID for filter context.
* @return WP_Post[] Array of posts ordered by newest first.
*/
public static function get_posts( $user_id ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a little thought: if this shows up in diagnostics or profiling data, it should have the class prepending in its name, but also calling it something like get_posts_on_this_day() would make that even clearer at a glance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed.

*
* Note: I made the trade-off to ignore `date_format` and `time_format`
* option changes. They do not bust the cache; stale date strings clear
* on the next post mutation or at midnight.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: we are going to need to rewrite this to remove the first-person dialog.

/**
 *
 * Note: Ignoring `date_format` and `time_format` option changes is intentional.
 *       This is because stale date strings clear on the next post
 *       mutation or next midnight hour.

though I think "clear on the next midnight" is a bit unclear to me. I’m not sure what all it means, so perhaps it could use some rewording, or perhaps I am just not familiar-enough with this to know.

* Builds the date query clause for today's anniversary date.
*
* On February 28 in a non-leap year, February 29 posts are included so
* leap-day anniversaries still appear.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this not backwards? since every year contains a February 28, but only every 4 years (less every hundred except every four hundred) contain a February 29?

I would expect that on Feb 28 we always see Feb 28 posts, but on Feb 29 we also include Feb 28

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. I think we have different product intentions. Without any special clauses, if you posted a post on Feb 29th, you won't be reminded about it for 4 years, but we want you to be.

So on 28th Feb of a non-leap year, we want to remind you about posts from Feb 29th because the next day wouldn't be one and you'd have to wait too long to see your posts from the last leap year.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here makes sense to me, per @alshakero's response.

<ul class="wp-on-this-day-years">
<?php foreach ( $posts_by_year as $year => $year_posts ) : ?>
<li class="wp-on-this-day-year">
<h3 class="wp-on-this-day-year-heading"><?php echo esc_html( $year ); ?></h3>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to add data-post-year on this? or some attribute or class name which would be useful to stylers?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand. Would you like me to add an attribute?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @dmsnell is just asking whether it might make sense to add an attribute that extenders can use for styling.

I'm not sure I can see a lot of reason for that; obviously people can potentially want to style any number of things, but it doesn't seem like a significant use case, to me.

It's obviously pretty harmless to add a class for the year (I don't think I'd use a data attribute for this), but I also don't see a lot of value in it. Could go either way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is less a request, and as @joedolson noted, an opportunity to ask if there’s a reason to add one

<a href="<?php the_permalink(); ?>"><?php echo esc_html( $title ); ?></a>
<?php if ( get_current_user_id() !== $author_id ) : ?>
<span class="wp-on-this-day-post-author">
<?php

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we intended this, but there’s a whitespace character implied here by the newline. this will return <span> by dmsnell</span> and not <span>by dmsnell</span>

we could capture it into a variable like $escaped_attribution and embed it directly to keep it clear without the space

echo "<span class='wp-on-this-day-post-author'>{$escaped_attribution}</span>";

@annezazu

annezazu commented Jul 3, 2026

Copy link
Copy Markdown

This plugin sends daily emails too, do we want that too?
The plugin adds a Gutenberg block

No, let's keep the scope small right now and just focus on the widget. I want to ensure this lands for 7.1 and the widget alone will be a nice value add.

@alshakero

Copy link
Copy Markdown
Author

@dmsnell thank you so much for the feedback implemented some and responded to some. On my end this is good to be merged. I'm happy to iterate on it after merging and own it for a few weeks if we come up with any feedback.

@annezazu

annezazu commented Jul 3, 2026

Copy link
Copy Markdown

Let's merge it!

@joedolson joedolson self-requested a review July 4, 2026 00:06

@peterwilsoncc peterwilsoncc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added quite a few notes inline....

One of my notes is over six parts so it looks larger than it is. With the changes following Matt's comment the display of the widget is much simplified so you can use the native caching in WP_Query instead of caching the markup.

That change removes a bunch of code which is why it's over six different suggestions.

Comment on lines +192 to +203
if ( 2 !== $month || 28 !== $day || $date->format( 'L' ) ) {
return $clause;
}

return array(
'relation' => 'OR',
$clause,
array(
'month' => 2,
'day' => 29,
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually encourage the early return pattern but I think this is on case in which it could be clearer to add the special case inside the condition.

Suggested change
if ( 2 !== $month || 28 !== $day || $date->format( 'L' ) ) {
return $clause;
}
return array(
'relation' => 'OR',
$clause,
array(
'month' => 2,
'day' => 29,
),
);
// Display leap day posts on Feb 28 in non leap years.
if (
28 === $day
&& 2 === $month
&& false === (bool) $date->format( 'L' )
) {
$clause = array(
'relation' => 'OR',
$clause,
array(
'month' => 2,
'day' => 29,
),
);
}
return $clause;

Comment on lines +280 to +281
'%s post has been published in a previous year:',
'%s posts have been published in previous years:',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'%s post has been published in a previous year:',
'%s posts have been published in previous years:',
'%s post has been published on this day in a previous year:',
'%s posts have been published on this day in previous years:',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to include the clause "in previous years" - that seems implied to me.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Added "on this day" and removed "in previous years".

Comment on lines +89 to +120
$user_id = get_current_user_id();

$cache_key = sprintf(
'render_otd_widget:v%d:%d:%s',
self::CACHE_VERSION,
$user_id,
determine_locale()
);
$cache_salt = array(
current_time( 'Y-m-d' ),
wp_cache_get_last_changed( 'posts' ),
);

$cached = wp_cache_get_salted( $cache_key, self::CACHE_GROUP, $cache_salt );
if ( ! is_string( $cached ) ) {
$posts = self::get_cached_posts( $user_id );

if ( empty( $posts ) ) {
return;
}

ob_start();
self::render_posts( $posts );
$cached = ob_get_clean();

wp_cache_set_salted( $cache_key, $cached, self::CACHE_GROUP, $cache_salt, DAY_IN_SECONDS );
}

echo '<div class="wp-on-this-day-widget">';
// Already escaped at write time by the render_* methods below.
echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part one of six.

Following the change to a list of posts, the render of the widget is now far less complex so I think it's safe to use the native caching in WP_Query rather than set up a specific cache for the widget. This will provide a couple of advantages:

  • on multi-user blogs the same cache will be hit be all users
  • The main WP_Query cache is for a very small amount of data -- in this widget it will be only the cache IDs.
  • The cache will be stored in the post-queries group so caching plugins with group flushing will handle it by default.

The docblock will need to be updated accordingly.

Suggested change
$user_id = get_current_user_id();
$cache_key = sprintf(
'render_otd_widget:v%d:%d:%s',
self::CACHE_VERSION,
$user_id,
determine_locale()
);
$cache_salt = array(
current_time( 'Y-m-d' ),
wp_cache_get_last_changed( 'posts' ),
);
$cached = wp_cache_get_salted( $cache_key, self::CACHE_GROUP, $cache_salt );
if ( ! is_string( $cached ) ) {
$posts = self::get_cached_posts( $user_id );
if ( empty( $posts ) ) {
return;
}
ob_start();
self::render_posts( $posts );
$cached = ob_get_clean();
wp_cache_set_salted( $cache_key, $cached, self::CACHE_GROUP, $cache_salt, DAY_IN_SECONDS );
}
echo '<div class="wp-on-this-day-widget">';
// Already escaped at write time by the render_* methods below.
echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo '<div class="wp-on-this-day-widget">';
self::render_posts( self::get_posts_on_this_day() );
echo '</div>';

Comment on lines +132 to +135
* @param int $user_id Current user ID for filter context.
* @return WP_Post[] Array of posts ordered by newest first.
*/
public static function get_posts_on_this_day( $user_id ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part two of six

Suggested change
* @param int $user_id Current user ID for filter context.
* @return WP_Post[] Array of posts ordered by newest first.
*/
public static function get_posts_on_this_day( $user_id ) {
* @return WP_Post[] Array of posts ordered by newest first.
*/
public static function get_posts_on_this_day() {

Comment on lines +163 to +166
* @param array $args WP_Query arguments.
* @param int $user_id Current user ID.
*/
$args = apply_filters( 'wp_dashboard_on_this_day_query_args', $args, $user_id );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part three of six

No longer used.

Suggested change
* @param array $args WP_Query arguments.
* @param int $user_id Current user ID.
*/
$args = apply_filters( 'wp_dashboard_on_this_day_query_args', $args, $user_id );
* @param array $args WP_Query arguments.
*/
$args = apply_filters( 'wp_dashboard_on_this_day_query_args', $args );

Comment on lines +43 to +51
/**
* Cache version. Bump when the rendered markup changes so stale
* entries from older releases are naturally ignored.
*
* @since 7.1.0
* @var int
*/
const CACHE_VERSION = 6;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part six of six

No longer used.

Suggested change
/**
* Cache version. Bump when the rendered markup changes so stale
* entries from older releases are naturally ignored.
*
* @since 7.1.0
* @var int
*/
const CACHE_VERSION = 6;

Comment on lines +27 to +34
/**
* Object cache group used for storing widget results.
*
* @since 7.1.0
* @var string
*/
const CACHE_GROUP = 'on_this_day';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part five of six

Suggested change
/**
* Object cache group used for storing widget results.
*
* @since 7.1.0
* @var string
*/
const CACHE_GROUP = 'on_this_day';

Comment thread src/wp-admin/includes/dashboard.php Outdated
Comment on lines +92 to +98
if ( current_user_can( 'edit_posts' ) ) {
if ( ! class_exists( 'WP_Dashboard_Widget_On_This_Day' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-dashboard-widget-on-this-day.php';
}

WP_Dashboard_Widget_On_This_Day::register_widget();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widget can be displayed to all users now it's blog wide. Similar to the activity widget.

Suggested change
if ( current_user_can( 'edit_posts' ) ) {
if ( ! class_exists( 'WP_Dashboard_Widget_On_This_Day' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-dashboard-widget-on-this-day.php';
}
WP_Dashboard_Widget_On_This_Day::register_widget();
}
if ( ! class_exists( 'WP_Dashboard_Widget_On_This_Day' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-dashboard-widget-on-this-day.php';
}
WP_Dashboard_Widget_On_This_Day::register_widget();

sprintf(
/* translators: %s: Post author's display name. */
__( 'by %s' ),
get_the_author()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few edge cases here:

  • get_the_author() can return an empty value if the display name is not set so you should either fallback to their user name or nice name.
  • It's possible for a post not to have an author set, ie post_author === 0, in which case you can skipe displaying the author

)
);

echo '<span class="wp-on-this-day-post-author">' . $escaped_attribution . '</span>';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove early escaping and escape upon echo. Escaping late is safer than escaping early as it protects developers from their later selves.

Suggested change
echo '<span class="wp-on-this-day-post-author">' . $escaped_attribution . '</span>';
echo '<span class="wp-on-this-day-post-author">' . esc_html( $escaped_attribution ) . '</span>';

@t-hamano t-hamano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the direction of this feature, I don't have a strong opinion, but from a code quality perspective, I feel there is still much room for improvement.

Comment thread src/wp-admin/css/on-this-day.css Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this widget require its own dedicated CSS file? Can't it be integrated into dashboard.css?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully agree with this.there's no reason not to combine this with existing CSS.

Comment thread src/wp-admin/css/on-this-day.css Outdated
Comment on lines +6 to +10
--wp-otd-widget-accent-rgb: var(--wp-admin-theme-color--rgb, 34, 113, 177);
--wp-otd-widget-muted: #646970;
--wp-otd-widget-pill: 9999px;
--wp-otd-widget-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96);
--wp-otd-widget-accent-8: rgba(var(--wp-otd-widget-accent-rgb), 0.08);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to define CSS custom properties that are only used once. It should be sufficient to apply the styles directly inline.

Comment thread src/wp-admin/css/on-this-day.css Outdated
Comment thread src/wp-admin/css/on-this-day.css Outdated
}

#wp_dashboard_on_this_day .wp-on-this-day-title::after {
content: attr(data-wp-otd-window-label);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check if placing content with meaning in the CSS content property causes any accessibility issues. Will all screen readers announce this date correctly?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, screen readers will announce generated content. This still creates a problem; it'll be announced as, e.g. On this dayJULY 6th". However, I don't really see why we would choose to do it this way; what's the barrier to having this directly in the HTML?

Comment thread src/wp-admin/css/on-this-day.css Outdated
Comment on lines +22 to +27
#wp_dashboard_on_this_day .wp-on-this-day-year-heading {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#wp_dashboard_on_this_day .wp-on-this-day-year-heading {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
}
#wp_dashboard_on_this_day .wp-on-this-day-year-heading {
font-weight: 600;
}

Remove redundant CSS overrides

Comment thread src/wp-admin/css/on-this-day.css Outdated
background: var(--wp-otd-widget-accent-8);
border-radius: var(--wp-otd-widget-pill);
white-space: nowrap;
vertical-align: 1px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid the approach of using vertical-align to align elements in new code going forward. This is an outdated method, and we should be able to use a flexbox layout.

* @since 7.1.0
* @var int
*/
const POSTS_PER_PAGE = -1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this appropriate? If there are a large number of posts on the same day in the past, won't they all be displayed on the dashboard, making the entire dashboard screen very long?

*
* @since 7.1.0
*/
class WP_Dashboard_Widget_On_This_Day {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why only this widget is implemented as a class. Shouldn't this be a function according to other implementations?

  • At a Glance → wp_dashboard_right_now()
  • Activity → wp_dashboard_site_activity()
  • Quick Draft → wp_dashboard_quick_press()
  • Events and News → wp_dashboard_events_news()
  • Site Health → wp_dashboard_site_health()

If we were to truly implement this widget as a class, I believe we should evaluate all its methods and avoid making them unnecessarily public.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I share this concern.

* @since 7.1.0
* @var int
*/
const CACHE_VERSION = 6;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is version control necessary for caches? Aren't caches reset every 24 hours?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching is gone now 👍🏼

Comment thread src/wp-admin/includes/class-wp-dashboard-widget-on-this-day.php Outdated
* @since 7.1.0
* @var int
*/
const POSTS_PER_PAGE = -1;

@peterwilsoncc peterwilsoncc Jul 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, forgot to log this. Unlimited queries can be bad so let's use a high default that's not unlimited.

(In a previous job I worked at a newspaper publishing hundreds of articles a day. They've been using WordPress as their CMS since 2016 so an unlimited query would end up returning at least 1000 articles. This would cause problems. ;)

Suggested change
const POSTS_PER_PAGE = -1;
const POSTS_PER_PAGE = 50;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this should be a smaller limit? From a practical standpoint, how many posts would somebody actually want to see in this widget? Once we're deciding not to just show them all, it seems like we should set more browseable limit, e.g. the top 10 posts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joedolson Yes, I think you are right. 50 would still make for a very long widget. Using the default value makes sense.

@joedolson joedolson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with previous reviews; there's a fair amount of room here to continue to improve.

Comment thread src/wp-admin/css/on-this-day.css Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully agree with this.there's no reason not to combine this with existing CSS.

Comment thread src/wp-admin/css/on-this-day.css Outdated
}

#wp_dashboard_on_this_day .wp-on-this-day-title::after {
content: attr(data-wp-otd-window-label);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, screen readers will announce generated content. This still creates a problem; it'll be announced as, e.g. On this dayJULY 6th". However, I don't really see why we would choose to do it this way; what's the barrier to having this directly in the HTML?

* @since 7.1.0
* @var int
*/
const POSTS_PER_PAGE = -1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this should be a smaller limit? From a practical standpoint, how many posts would somebody actually want to see in this widget? Once we're deciding not to just show them all, it seems like we should set more browseable limit, e.g. the top 10 posts.

* Builds the date query clause for today's anniversary date.
*
* On February 28 in a non-leap year, February 29 posts are included so
* leap-day anniversaries still appear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here makes sense to me, per @alshakero's response.

Comment on lines +280 to +281
'%s post has been published in a previous year:',
'%s posts have been published in previous years:',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to include the clause "in previous years" - that seems implied to me.

<ul class="wp-on-this-day-years">
<?php foreach ( $posts_by_year as $year => $year_posts ) : ?>
<li class="wp-on-this-day-year">
<h3 class="wp-on-this-day-year-heading"><?php echo esc_html( $year ); ?></h3>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @dmsnell is just asking whether it might make sense to add an attribute that extenders can use for styling.

I'm not sure I can see a lot of reason for that; obviously people can potentially want to style any number of things, but it doesn't seem like a significant use case, to me.

It's obviously pretty harmless to add a class for the year (I don't think I'd use a data attribute for this), but I also don't see a lot of value in it. Could go either way.

?>
<li class="wp-on-this-day-post">
<a href="<?php the_permalink(); ?>"><?php echo esc_html( $title ); ?></a>
<?php if ( get_current_user_id() !== $author_id ) : ?>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a lot of value in omitting the current user from this data. For single author sites it makes sense, as all authors are the same person; but for heavily multi-author sites it just looks strange to me for some posts to omit author information.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but the vast majority of sites are single-author, so this will be noise for most people.

Comment thread src/wp-admin/includes/dashboard.php Outdated
}

// On This Day.
if ( current_user_can( 'edit_posts' ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. There's nothing non-public about this.

wp_add_dashboard_widget(
self::WIDGET_ID,
sprintf(
'<span class="wp-on-this-day-title" data-wp-otd-window-label="%s">%s</span>',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we aren't rendering the date directly in HTML. It makes the text unselectable.

I assume it's to avoid problems with justify-content: space-between on the heading? That can be gotten around by nesting elements, too, so it's not necessary.

I'm just not sure we should be making it habitual to use CSS generated readable content.

*
* @since 7.1.0
*/
public static function register_widget() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It calls through to wp_add_dashboard_widget, so I think it should follow a similar naming convention. add_dashboard_widget or even just add.

Also it'd be good to disambiguate from the existing register_widget - https://developer.wordpress.org/reference/functions/register_widget/, as that's for the other type of widget.

*
* @since 7.1.0
*/
public static function render_dashboard_widget() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be public?

If it's this way for the test, then I think there are examples across that codebase of using reflection for this.

@t-hamano

t-hamano commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@alshakero, Do you have the bandwidth to address the feedback? If not, someone else can help with the task 🙂

@alshakero

Copy link
Copy Markdown
Author

Hi @t-hamano! I'm on this now. I was on a short holiday.

@alshakero

alshakero commented Jul 8, 2026

Copy link
Copy Markdown
Author

Thank you for the feedback. I admit, this is much neater now. Summary of the changes:

@peterwilsoncc's review:

  • Removed the custom caching layer (cache constants, salted markup/posts cache, get_cached_posts()) in favor of WP_Query's native post-queries caching.
  • Made the widget available to all users by removing the current_user_can( 'edit_posts' ) check in dashboard.php
  • Fixed author attribution edge cases — the "by X" span is skipped when post_author is 0 or the display name is empty
  • Restructured the leap-day logic in the date query clause to put the special case inside an if instead of an early return
  • Moved escaping of the author next to the echoing.

@t-hamano's review:

  • Converted the class-based implementation to plain functions matching other dashboard widgets (wp_dashboard_on_this_day() etc.), kept in a separate file, with internal helpers made private via underscore prefix and @access private
  • Capped the post list at 10 (was unlimited), still adjustable via the query-args filter; added a test for the cap
  • Deleted the dedicated on-this-day.css file and its script-loader registrations, folding the styles into dashboard.css
  • Fixed the date pill accessibility by rendering the date as a real <span> in the title instead of CSS content: attr()
  • CSS cleanups: removed one-use custom properties, trimmed the year-heading rule to font-weight: 600, simplified selectors to element-based where unambiguous, and replaced vertical-align with flexbox.

@joedolson's comments:

  • Changed the phrasing to the cleaner "N posts has been published on this day", without "previous years".
  • Moved the date pill into its own span. My mistake was testing two elements which broke the layout when I tried it. With nesting it works great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.