Proposal: Add On This Day Widget - #11630
Conversation
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
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 Unlinked AccountsThe 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: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
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. |
|
Tested locally and it loads just fine! |
|
Redesigned following this.
|
jeherve
left a comment
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Why is allow dynamic properties needed? As it is new code, I would refrain from adding this.
There was a problem hiding this comment.
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.
|
Thanks for the amazing feedback, @jeherve!
Done.
I added a minimal slider to keep the noise down the allows adjusting the range from 1 to 7 days.
Fixed. |
dmsnell
left a comment
There was a problem hiding this comment.
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() ) ) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
OK I side stepped this by avoiding CSS props in favor of HTML attributes.
| * @since 7.1.0 | ||
| * @var int | ||
| */ | ||
| const POSTS_PER_PAGE = -1; |
There was a problem hiding this comment.
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. ;)
| const POSTS_PER_PAGE = -1; | |
| const POSTS_PER_PAGE = 50; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@joedolson Yes, I think you are right. 50 would still make for a very long widget. Using the default value makes sense.
joedolson
left a comment
There was a problem hiding this comment.
Agree with previous reviews; there's a fair amount of room here to continue to improve.
There was a problem hiding this comment.
Fully agree with this.there's no reason not to combine this with existing CSS.
| } | ||
|
|
||
| #wp_dashboard_on_this_day .wp-on-this-day-title::after { | ||
| content: attr(data-wp-otd-window-label); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
The logic here makes sense to me, per @alshakero's response.
| '%s post has been published in a previous year:', | ||
| '%s posts have been published in previous years:', |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 ) : ?> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes but the vast majority of sites are single-author, so this will be noise for most people.
| } | ||
|
|
||
| // On This Day. | ||
| if ( current_user_can( 'edit_posts' ) ) { |
There was a problem hiding this comment.
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>', |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
|
@alshakero, Do you have the bandwidth to address the feedback? If not, someone else can help with the task 🙂 |
|
Hi @t-hamano! I'm on this now. I was on a short holiday. |
|
Thank you for the feedback. I admit, this is much neater now. Summary of the changes: @peterwilsoncc's review:
@t-hamano's review:
@joedolson's comments:
|
peterwilsoncc
left a comment
There was a problem hiding this comment.
Thank you so much for all your work on this, I understand the review process can be a lot but everyone's working toward the same goal.
I've noticed a few more things:
- reducing db queries
- a minor copy change for a single post
- the remaining comments are for an edge case when the dashboard is customised by users, either the widget is hidden or the location set by the user.
| printf( | ||
| esc_html( | ||
| /* translators: %s: Number of posts. */ | ||
| _n( | ||
| '%s post has been published on this day:', | ||
| '%s posts have been published on this day:', | ||
| $post_count | ||
| ) | ||
| ), | ||
| esc_html( number_format_i18n( $post_count ) ) | ||
| ); |
There was a problem hiding this comment.
This is a pretty common pattern in the default themes for comments, see example in 2010. The else condition uses _n() as pluralization rules differ in various languages.
| printf( | |
| esc_html( | |
| /* translators: %s: Number of posts. */ | |
| _n( | |
| '%s post has been published on this day:', | |
| '%s posts have been published on this day:', | |
| $post_count | |
| ) | |
| ), | |
| esc_html( number_format_i18n( $post_count ) ) | |
| ); | |
| if ( 1 === $post_count ) { | |
| esc_html_e( 'One post has been published on this day:' ); | |
| } else { | |
| printf( | |
| esc_html( | |
| /* translators: %s: Number of posts. */ | |
| _n( | |
| '%s post has been published on this day:', | |
| '%s posts have been published on this day:', | |
| $post_count | |
| ) | |
| ), | |
| esc_html( number_format_i18n( $post_count ) ) | |
| ); | |
| } |
| if ( empty( $posts ) ) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Without a placeholder, this can reset sorting/dragging & dropping widgets on the dashboard but it takes a little work to reproduce.
- Move the OTD widget below the WordPress Events and News widget.
- Edit the dates of all the posts displayed in the widget to the day before
- Reload the dashboard, the OTD widget is hidden.
- Swap the positions of the at a glance and site health widgets
- Reload the dashboard
- Edit the date of a post so it will appear in the OTD widget
- Reload the dashboard
- The OTD widget returns to the default position.
Another effect is that if there are no posts on this day, the widget does not appear in the site options tab allowing users to hide the widget if they wish to do so.
It uses a CSS hack but I was able to fix it with the following:
| if ( empty( $posts ) ) { | |
| return; | |
| } | |
| $title_class = 'wp-on-this-day-title'; | |
| if ( empty( $posts ) ) { | |
| $title_class .= ' wp-on-this-day-title-empty'; | |
| } |
Additionally:
- Modify the registration of the widget to use the dynamic class in the title (remember to use
esc_attr()) - Add the following CSS to the dashboard styles:
#wp_dashboard_on_this_day:has(.wp-on-this-day-title-empty) { display: none; }
There was a problem hiding this comment.
This seems like a reasonable solution to do this, but wouldn't it be simpler to do $title_class .= ' hidden';, and inherit existing styles?
| wp_add_dashboard_widget( | ||
| 'wp_dashboard_on_this_day', | ||
| sprintf( | ||
| '<span class="wp-on-this-day-title">%s <span class="wp-on-this-day-date">%s</span></span>', |
| wp_add_dashboard_widget( | ||
| 'wp_dashboard_on_this_day', | ||
| sprintf( | ||
| '<span class="wp-on-this-day-title">%s <span class="wp-on-this-day-date">%s</span></span>', |
|
@t-hamano addressed all. |
|
@alshakero Thanks for the update. I have no further feedback from my end, but we might need to address the following outstanding points. Disable the meta cache for the query: #11630 (comment) If the widget itself is not registered due to an early return, the dashboard's sort order will be reset: #11630 (comment) Should we hide my author name on my posts? #11630 (comment) The date badge has been moved from the title area to the content area
This was my suggestion, but if we want to maintain the previous design, I think we should hide the badge displayed in the screen options.
|
|
Addressed all @peterwilsoncc's feedback. |
I addressed that here. IMO yes, the vast majority of sites are single-author so rendering the author will be noise for most users. |
t-hamano
left a comment
There was a problem hiding this comment.
Personally, I don't have any further feedback at this time. If there's any additional feedback from others, please feel free to comment.
joedolson
left a comment
There was a problem hiding this comment.
Possible tweak; but I think this is essentially ready to go.
| function wp_dashboard_on_this_day_setup() { | ||
| $title_class = 'wp-on-this-day-title'; | ||
| if ( empty( wp_dashboard_on_this_day_get_posts() ) ) { | ||
| $title_class .= ' wp-on-this-day-title-empty'; |
There was a problem hiding this comment.
I didn't verify that it would work, but I commented in Peter's review notes that it seems simpler to use $title_class .= ' hidden'; and omit the extra CSS. But only if the CSS specificity will allow that to work.
There was a problem hiding this comment.
We don't really want to hide the title itself, we use parent selectors to hide the whole parent postbox using this classname. So hidden wouldn't work. I switched to a filter to add the hidden classname on the parent though.
|
Great work, all. Thanks for the continued effort in particular @alshakero 👏🏼. |







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
On This Day · <Month Day>and appears in the dashboard grid for users withedit_posts.2023 · 3 yrs) and the posts published that day as cards with excerpt, time, categories, and Edit/View links.Viewlink to the permalink.Screenshots
Testing
npm run env:start, thennpm run env:install.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.