Skip to content

Proposal: Add On This Day Widget#11630

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

Proposal: Add On This Day Widget#11630
alshakero wants to merge 49 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
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.

@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, kellychoffman, simison, joen, retrofox, matt, sabernhardt.

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

Copy link
Copy Markdown
Author

@dmsnell I implemented the list of inline tags and added some tests.

@aaronjorbin aaronjorbin 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.

Thanks for working on this. A handleful of comments are inline. Additionally:

  1. It's a bit jarring to jump to the top of the page when adjusting the slider.
  2. What I'm seeing in the output seems to not be what is intended, so I haven't reviewed that at all:
Image
  1. 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;

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.

Why isn't this 0 or 1 if this is a new feature?

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 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(

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 should likely use PHP 8.6's clamp() function. See https://core.trac.wordpress.org/ticket/65143

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 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!' ),

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.

Suggested change
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

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

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

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_blog_admin is unnecessary since if they are in this file, it's the admin.

Suggested change
if ( is_blog_admin() && current_user_can( 'edit_posts' ) ) {
if ( current_user_can( 'edit_posts' ) ) {

@alshakero

alshakero commented May 12, 2026

Copy link
Copy Markdown
Author

Hi Aaron! I much appreciate your review. Thank you.

It's a bit jarring to jump to the top of the page when adjusting the slider.

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.

What I'm seeing in the output seems to not be what is intended, so I haven't reviewed that at all:

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?

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.

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.

@alshakero

alshakero commented May 12, 2026

Copy link
Copy Markdown
Author

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.

@kellychoffman

Copy link
Copy Markdown

@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: 


  • Only show published posts. Remove drafts. 

  • Make Reblog the primary action. Secondary action is to view the post. Tertiary action is to Share, which would copy a link to the clipboard. I think we should encourage blogs and blogging with this!
  • Most people will likely only have one blog post for any given day, if they even have any. Optimize the design for that.
Get rid of the slider at the bottom of the post and have the 7 day range be the default. This is what other competitors do as well.
  • If there are multiple, still show only one at a time, but introduce a way to flip through each post.
  • If there is a, a featured image, or gallery in the post, include the first image in the widget. People love photos and are more likely to engage with the widget.
  • I'd further clean up the UI by only showing the date, title, excerpt or first few lines of the photo. Remove the rest of the post meta. Put the post date closer to the 'x years ago' ui.

Screenshots of suggested updates:

If only one post:

Screenshot 2026-05-12 at 2 29 07 PM

If multiple posts, no photo:
Screenshot 2026-05-12 at 8 45 27 PM

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!" ),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
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

alshakero added 2 commits May 13, 2026 14:07
1. Move to a carousel.
2. Fixed 7 days window.
3. Use jQuery for JS.
4. Add a share button.
@alshakero

alshakero commented May 13, 2026

Copy link
Copy Markdown
Author

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 post-new.php. It's a couple of lines but felt really out of scope.

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.

Request

Regarding the empty state, I don't love the design. It's centered (vs start-aligned) and too tall IMO. WDYT?
image

@simison

simison commented May 18, 2026

Copy link
Copy Markdown
Member

@kellychoffman @alshakero, have you also looked at designs proposed at WordPress/gutenberg#74936 ?

There are some neat visual ideas

image

cc @jameskoster @jasmussen

@jasmussen

jasmussen commented May 19, 2026

Copy link
Copy Markdown

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.

@retrofox

Copy link
Copy Markdown

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.

@alshakero

alshakero commented May 20, 2026

Copy link
Copy Markdown
Author

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.

image

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

image

Different

image

@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 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">

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.

Allow users to decide to open in new tab.

🔢 This applies to this and the links elsewhere.

Suggested change
<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',

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
'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,

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.

No need to prime term caches update_post_term_cache => false

* @output wp-admin/js/on-this-day.js
*/

( function( $ ) {

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'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/

@alshakero alshakero Jun 18, 2026

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.

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.

Comment thread src/wp-admin/css/on-this-day.css Outdated
follows the user's selected admin color scheme (Blue, Modern, Coffee, etc.).
Fallback values match the classic "Fresh" scheme.
----------------------------------------------------------------------------- */
#dashboard_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'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() {

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.

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 ) ) {

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.

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,

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.

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 );

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 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' ) ) {

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.

Along with my comment above re displaying posts of all authors, this can be shown to all users.

@alshakero

Copy link
Copy Markdown
Author

Thank you so much @peterwilsoncc! I addressed all your feedback.

@m

m commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Quick thoughts:

@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.

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.