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 thank you so much for the review. Addressed all feedback. |
| 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 . '"'; | ||
| } |
There was a problem hiding this comment.
The UTF-8 scrub is a good idea. Some of the escaping isn't correct or ideal. Notably:
\r\n,\r,\f, and\nshould all be normalized to the\aescape.
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.
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>
d281f49 to
5b46861
Compare
dmsnell
left a comment
There was a problem hiding this comment.
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' ) |
| esc_html( $start_label ) | ||
| ); | ||
|
|
||
| if ( self::MIN_WINDOW_DAYS === $window_days ) { |
There was a problem hiding this comment.
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. | ||
| */ |
There was a problem hiding this comment.
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, '…' ); |
There was a problem hiding this comment.
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' ); ?> |
There was a problem hiding this comment.
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>



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.Notes for reviewers
WP_Querycall intentionally uses aposts_wherefilter scoped to a single query to matchMM-DDin an index-friendly way rather than inflatingmeta_query/date_query.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.