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.
…/wordpress-develop into add/on-this-day-widget
|
@dmsnell I implemented the list of inline tags and added some tests. |
aaronjorbin
left a comment
There was a problem hiding this comment.
Thanks for working on this. A handleful of comments are inline. Additionally:
- It's a bit jarring to jump to the top of the page when adjusting the slider.
- What I'm seeing in the output seems to not be what is intended, so I haven't reviewed that at all:
- It feels like a lot of the functionality for excerpts isn't using core functionality. If core needs to work around core limitations, that is a good opportunity to make enhacements so they are available elsewhere. That work likely should happen in a different ticket that blocks this one.
| * @since 7.1.0 | ||
| * @var int | ||
| */ | ||
| const CACHE_VERSION = 11; |
There was a problem hiding this comment.
Why isn't this 0 or 1 if this is a new feature?
There was a problem hiding this comment.
This is for my own testing. Changing back to 0.
| * @return int Number of days to include, between 1 and 7. | ||
| */ | ||
| protected static function clamp_window_days( $window_days ) { | ||
| return min( |
There was a problem hiding this comment.
This should likely use PHP 8.6's clamp() function. See https://core.trac.wordpress.org/ticket/65143
There was a problem hiding this comment.
I agree in general, but I'll keep it in this case until #11669 is merged to make testing this easier.
|
|
||
| printf( | ||
| /* translators: 1: Start date, 2: End date. */ | ||
| esc_html__( 'You haven\'t published anything between %1$s and %2$s in previous years. Write something today and check back next year!' ), |
There was a problem hiding this comment.
| esc_html__( 'You haven\'t published anything between %1$s and %2$s in previous years. Write something today and check back next year!' ), | |
| esc_html__( "You haven't published anything between %1$s and %2$s in previous years. Write something today and check back next year!" ), |
Avoid the need to escape the single quote: https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#single-and-double-quotes
| } | ||
|
|
||
| // On This Day. | ||
| if ( is_blog_admin() && current_user_can( 'edit_posts' ) ) { |
There was a problem hiding this comment.
is_blog_admin is unnecessary since if they are in this file, it's the admin.
| if ( is_blog_admin() && current_user_can( 'edit_posts' ) ) { | |
| if ( current_user_can( 'edit_posts' ) ) { |
|
Hi Aaron! I much appreciate your review. Thank you.
Yes, I didn't want to add any JS to make this dynamic because it's performance-sensitive. I now added the element's ID in the URL hash to scroll back down to it when it's updated. Much nicer now.
I'm unable to reproduce this which makes it interesting. Maybe you have some special HTML in your posts that is breaking my escaping. If this is a testing site, would you mind sharing an export?
I think Core has great tooling around excerpts. But we went a bit extreme with this one to optimize performance. You can see our discussion here. I addressed all the inline comments. |
|
Update: I managed to reproduce this. The minifier is mangling the CSS file because it doesn't support native CSS nesting. Even though it is supported everywhere now. I fixed that now by flattening the CSS file. |
|
@alshakero Love this! I have some thoughts and suggestions for the design of this. What if we pare it back a bit and ship a very clean, opinionated widget? Then, add more as people use it and you get feedback on the feature. I think its easier to add than to take away. Suggested changes:
Screenshots of suggested updates: If only one post:
Still some UI details to work out, but what do you think? If you want to take it for a spin, I have a branch here: https://github.com/kellychoffman/wordpress-develop/tree/kelly/otd-carousel-alt |
|
|
||
| printf( | ||
| /* translators: 1: Start date, 2: End date. */ | ||
| esc_html__( "You haven't published anything between %1$s and %2$s in previous years. Write something today and check back next year!" ), |
There was a problem hiding this comment.
| esc_html__( "You haven't published anything between %1$s and %2$s in previous years. Write something today and check back next year!" ), | |
| esc_html__( 'You haven\'t published anything between %1$s and %2$s in previous years. Write something today and check back next year!' ), |
This should solve the phpcs and phpstan errors by using the single quotes and escaping the single quote in haven\'t
|
Hi @kellychoffman! Thank you so much, this is great. In retrospect, it's obvious that it won't be common to have multiple posts on a given day. I adopted your design. The only missing thing is that I didn't add a reblog button. That is not supported by default and I would have to add some filters to I also added a native share button instead of just copying. There is a Web Share API now and it's perfect for this case. It takes care of copying too. I did failover to copying for older browsers and Firefox. RequestRegarding the empty state, I don't love the design. It's centered (vs start-aligned) and too tall IMO. WDYT? |
|
@kellychoffman @alshakero, have you also looked at designs proposed at WordPress/gutenberg#74936 ? There are some neat visual ideas
|
|
Thank you for the ping! Given the core context here, and that we've already on that other issue received some questions around what's plugin territory vs. core, that's a good nuance to unstick here as well. Which is to say, it feels to me very appropriate to have an "On this day" widget, especially since it is being paired with a larger dashboard refresh that I know @retrofox has been looking into. But we probably shouldn't have two different ones. I happen to like quite a lot what @jarekmorawski did with the other one, though I wonder if there's a way to merge these two efforts? The prompt for an empty period feels like a nice addition. The "reblog" button is an open question for extensibility, though, it'd be nice if plugins could tap into this widget. |
|
Yes, I think we can join efforts here. Also, extensibility is a nice one. Let's suppose you have a historical events plugin in which the data source isn't simply a post. We should allow consumers to extend the events of this day. |
|
Hi @kellychoffman @jarekmorawski! It would be wonderful if we could reach an agreement on this, the momentum is great (for now). I took inspiration from the empty state from Jarek's work, it's much better than the version that I had.
I did have an issue the padding though. The Quick Draft widget has 12px padding, and when I add more, our widget looks out of place. But when I make them consistent, it looks bad IMO Consistent
Different
|
peterwilsoncc
left a comment
There was a problem hiding this comment.
I've added some notes inline.
| <div class="on-this-day-post-row"> | ||
| <?php if ( $image_url ) : ?> | ||
| <div class="on-this-day-post-image"> | ||
| <a href="<?php echo esc_url( $view_link ); ?>" target="_blank" rel="noopener" tabindex="-1" aria-hidden="true"> |
There was a problem hiding this comment.
Allow users to decide to open in new tab.
🔢 This applies to this and the links elsewhere.
| <a href="<?php echo esc_url( $view_link ); ?>" target="_blank" rel="noopener" tabindex="-1" aria-hidden="true"> | |
| <a href="<?php echo esc_url( $view_link ); ?>" tabindex="-1" aria-hidden="true"> |
| $user_id = get_current_user_id(); | ||
|
|
||
| $cache_key = sprintf( | ||
| 'render:v%d:%d:%s:%s:%s', |
There was a problem hiding this comment.
| 'render:v%d:%d:%s:%s:%s', | |
| 'render_otd_widget:v%d:%d:%s:%s:%s', |
| 'orderby' => 'date', | ||
| 'order' => 'DESC', | ||
| 'no_found_rows' => true, | ||
| 'date_query' => $date_query, |
There was a problem hiding this comment.
No need to prime term caches update_post_term_cache => false
| * @output wp-admin/js/on-this-day.js | ||
| */ | ||
|
|
||
| ( function( $ ) { |
There was a problem hiding this comment.
I'm not seeing anything that requires jQuery in this file (correct me if I am wrong) so it would be good to avoid the dependency. https://youmightnotneedjquery.com/
There was a problem hiding this comment.
It is not a requirement per se, but I started with vanilla JS, and the code was longer and too verbose. I then compared to other widgets and everyone except password-toggle uses jQuery.
| follows the user's selected admin color scheme (Blue, Modern, Coffee, etc.). | ||
| Fallback values match the classic "Fresh" scheme. | ||
| ----------------------------------------------------------------------------- */ | ||
| #dashboard_on_this_day { |
There was a problem hiding this comment.
I'd prefer it if all the IDs, classes were prefixed with wp_. WordPress hasn't been great about prefixing in the past but for new features it's preferable to.
| * | ||
| * @return array[] Date query clauses. | ||
| */ | ||
| protected static function get_window_date_query_clauses() { |
There was a problem hiding this comment.
Please relocate this to be just after the method creating the WP_Query so it's near the location it's used.
| } | ||
| } | ||
|
|
||
| if ( ! empty( $post->post_content ) ) { |
There was a problem hiding this comment.
For the post content, use get_the_content() and run it through the the_contentfilter. This will ensure that any data that is private is stripped, blocks and shortcodes are rendered.
If the images are using a block/shortcode this will increases the chance they are found.
| } | ||
|
|
||
| $excerpt = self::extract_excerpt_text( | ||
| has_excerpt( $post ) ? $post->post_excerpt : $post->post_content, |
There was a problem hiding this comment.
Use get_the_excerpt() and get_the_content() with the latter run through the the_content filter.
| ); | ||
|
|
||
| $current_year = (int) current_time( 'Y' ); | ||
| $post_year = (int) get_the_date( 'Y', $post ); |
There was a problem hiding this comment.
This can be removed and the code below use human_time_diff(). See this example in the Gutenberg repo https://github.com/WordPress/gutenberg/blob/e4b6b0ba8470dfd0e51fdc34343cf172bfb22fb1/packages/block-library/src/post-date/index.php#L63-L69
| } | ||
|
|
||
| // On This Day. | ||
| if ( current_user_can( 'edit_posts' ) ) { |
There was a problem hiding this comment.
Along with my comment above re displaying posts of all authors, this can be shown to all users.
|
Thank you so much @peterwilsoncc! I addressed all your feedback. |
|
Quick thoughts:
|
2664880 to
b4c40ed
Compare
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.
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?
|
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. |










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
Used for: The code is ~80% 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.