Skip to content

Proposal: Add On This Day Widget#11630

Open
alshakero wants to merge 28 commits intoWordPress:trunkfrom
alshakero:add/on-this-day-widget
Open

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

Conversation

@alshakero
Copy link
Copy Markdown

@alshakero alshakero commented Apr 22, 2026

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.

Notes for reviewers

  • The WP_Query call intentionally uses a posts_where filter scoped to a single query to match MM-DD in an index-friendly way rather than inflating meta_query/date_query.
  • The class is loaded lazily inside wp_dashboard_setup() (basically copied the Site Health loader) so it has no cost on non-dashboard admin screens.

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 100% 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
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

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.

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
Copy link
Copy Markdown
Author

alshakero commented Apr 23, 2026

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

Copy link
Copy Markdown

@jeherve jeherve left a comment

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]
Copy link
Copy Markdown

@apermo apermo Apr 25, 2026

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
Copy link
Copy Markdown
Author

alshakero commented Apr 25, 2026

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.

Copy link
Copy Markdown
Member

@dmsnell dmsnell left a comment

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 thank you so much for the review. Addressed all feedback.

Comment thread src/wp-admin/includes/class-wp-on-this-day.php Outdated
Comment on lines +350 to +359
protected static function esc_css_string( $value ) {
$value = wp_scrub_utf8( (string) $value );
$value = str_replace(
array( '\\', '"', "\n", "\r", "\f" ),
array( '\\\\', '\"', '\a ', '\d ', '\c ' ),
$value
);

return '"' . $value . '"';
}
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.

The UTF-8 scrub is a good idea. Some of the escaping isn't correct or ideal. Notably:

  • \r\n, \r, \f, and \n should all be normalized to the \a escape.

WordPress 7.0 makes inline styles much safer, but it may also be a good idea to escape HTML-like syntax <, >, and &. In case KSES is ever applied to this type of content, that will prevent the content from being mangled.

I think it's a good idea to prefer the Unicode escape sequences over the backslash-escapes (\22 instead of \". This ensures that the problematic characters don't appear at all in output and is less likely to confuse other systems like KSES.

For CSS string escaping, this is the implementation I've been using. You can probably omit most of the CSS syntax characters here, but be sure to keep the quotes!

$escaped = strtr(
	$value,
	array(
		// Escape existing backslashes to prevent unintentional escapes in result.
		'\\'   => '\\5C ',


		// Pre-processing replaces NULLs and some newlines. Replace and escape as necessary.
		"\0"   => "\u{FFFD}",


		// Normalize and replace newlines. https://www.w3.org/TR/css-syntax-3/#input-preprocessing
		"\r\n" => '\\A ',
		"\r"   => '\\A ',
		"\f"   => '\\A ',


		// Newlines must be escaped in CSS strings.
		"\n"   => '\\A ',


		// Arbitrary characters for Unicode escaping:


		// HTML syntax may be problematic.
		'<'    => '\\3C ',
		'>'    => '\\3E ',
		'&'    => '\\26 ',


		// CSS syntax may be problematic.
		','    => '\\2C ',
		';'    => '\\3B ',
		'{'    => '\\7B ',
		'}'    => '\\7D ',
		'"'    => '\\22 ',
		"'"    => '\\27 ',
	)
);
return "\"{$escaped}\"";

I have some draft work in sirreal#33 including the escaping I shared above. I hoped to propose for 7.1 to include functionality like this via WP_CSS_Builder::string( string $value ): string.

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.

Thanks for the elaborate explanation! I switched to HTML attribs instead. Didn't feel worth it to bundle CSS escaping just for this. Works great now in both LTR and RTL.

image

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.

Good choice 🙂

@escapemanuele
Copy link
Copy Markdown

escapemanuele commented Apr 28, 2026

Just noting that there might be a problem with the minimize button

image

I'll try to see what's wrong 👍

UPDATE: Fixed ✅

Scope the `.inside` flex layout to `:not(.closed)` so the global
`.js .closed .inside { display: none }` rule wins when the postbox
is minimized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@escapemanuele escapemanuele force-pushed the add/on-this-day-widget branch from d281f49 to 5b46861 Compare April 28, 2026 09:50
Copy link
Copy Markdown
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

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

The use of data- attributes presents a clearer data flow IMO. It makes the CSS less fragile as well.

sprintf(
'<span class="on-this-day-title" data-otd-window-label="%s">%s</span>',
esc_attr( self::get_window_label( self::get_window_days() ) ),
__( 'On This Day' )
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.

missing escapes

esc_html( $start_label )
);

if ( self::MIN_WINDOW_DAYS === $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.

the direct thing we’re intending here is a render-time language choice of one or not one. as written, it communicates that we are checking minimum or not minimum, whereby presumably the minimum could be more than one day.

it may feel like hard-coding constants here, but if ( 1 === $window_days ) is the most robust and direct way to communicate the intention of this.

*
* @param WP_Post $post Post object to render.
* @param int $window_days Number of days included in the date window.
*/
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.

the output from this is used as if already escaped for HTML, but no mention is made in this docblock. it’s uneasy enough passing around “escaped” content, so we should make it abundantly clear that this contains rendered and escaped HTML so it doesn’t get double-escaped.

$excerpt = has_excerpt( $post ) ? $post->post_excerpt : $post->post_content;
$excerpt = wp_strip_all_tags( strip_shortcodes( $excerpt ) );
$excerpt = preg_replace( '/\s+/', ' ', $excerpt );
$excerpt = wp_trim_words( trim( $excerpt ), 24, '&hellip;' );
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.

in a follow-up PR I’d love to share how we can use the HTML API to make this more reliable, faster, and safer. if you want to do it in this PR I’m happy to oblige, but it’s not a pressing matter we can’t follow-up on.

<?php endif; ?>
<div class="on-this-day-post-body">
<span class="screen-reader-text">
<?php echo $is_private ? esc_html__( 'Private post' ) : esc_html__( 'Published post' ); ?>
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 note on some of these where we are using newlines to make the PHP cleaner: this inserts a space character before the Private post label because newlines are normalized to spaces in HTML.

It’s worth reviewing the full PR for cases like this. The resolution is easy: move the <?php up a line or do it all on one line.

<span class="…"><?php
	echo $is_private ? … : …
?></span>
<span class="…"><?php echo $is_private ? esc_html__( … ) : esc_html__( … ); ?></span>

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.

6 participants