Skip to content

fix(blog): compare full ISO timestamps when sorting, and drop posts without a slug - #1654

Merged
aka-sacci-ccr merged 3 commits into
mainfrom
fix-blog-date-sorting
Aug 5, 2026
Merged

fix(blog): compare full ISO timestamps when sorting, and drop posts without a slug#1654
aka-sacci-ccr merged 3 commits into
mainfrom
fix-blog-date-sorting

Conversation

@aka-sacci-ccr

@aka-sacci-ccr aka-sacci-ccr commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Two fixes to blog/core/handlePosts.ts.


1. Date sorting ignored full ISO timestamps

The date branch of the sortPosts comparator built a Date by concatenating a time onto whatever was in post.date:

new Date(`${b.date}T00:00:00`).getTime() - new Date(`${a.date}T00:00:00`).getTime()

That assumes every date is exactly YYYY-MM-DD. But BlogPost.date is just a string and the CMS also stores full ISO timestamps. For "2026-08-05T12:51:59Z" the concatenation yields "2026-08-05T12:51:59ZT00:00:00"Invalid DategetTime() is NaN → the comparison is NaN.

Per spec (SortCompare: "If v is NaN, return +0") a NaN comparator result is treated as +0, and toSorted is stable — so a post with a full ISO timestamp was never moved. It just stayed wherever the records happened to put it. Symptom: BlogpostList with sortBy: "date_desc" listed yesterday's post above today's.

Timezone dependence, in two rounds

The same expression made ordering depend on the server timezone, because "2026-08-04T00:00:00" carries no timezone designator and is therefore parsed as local midnight.

Fixing that for bare dates alone turned out to be insufficient (review thread): per spec a date-only string is parsed as UTC, but a datetime with no designator is parsed as local. So an offset-less "2026-08-05T23:30:00" still ordered inconsistently against a bare "2026-08-06":

TZ=UTC                  desc: data-pura > sem-offset
TZ=Asia/Tokyo           desc: data-pura > sem-offset
TZ=America/Sao_Paulo    desc: sem-offset > data-pura   <-- flipped
TZ=Pacific/Kiritimati   desc: data-pura > sem-offset

Fix

Any ISO date or datetime lacking a timezone designator is pinned to UTC; strings that already carry a Z or a ±hh:mm offset pass through untouched. The pattern covers minute precision and fractional seconds, not just whole seconds:

const ISO_WITHOUT_TIMEZONE =
  /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?$/;

The || 0 keeps NaN from ever reaching the comparator, so an unparseable date sorts last for desc — consistent with the existing !a[sortMethod] guards, which already push missing values to the end.

Verified against the real sortPosts, including with the input order flipped — since the old behavior depended on the order records arrived in, passing in both directions is what shows it is actually sorting rather than getting lucky:

desc:           [ "2026-08-05T12:51:59Z", "2026-08-04" ]
asc :           [ "2026-08-04", "2026-08-05T12:51:59Z" ]
desc (flipped): [ "2026-08-05T12:51:59Z", "2026-08-04" ]

Every offset-less form resolves to the same instant as its explicit-UTC twin, and the day-boundary ordering above is stable across all five timezones tested.


2. Posts without a slug were rendered in listings

A post whose slug is missing, empty or blank has no route, so it can never be rendered — but it still appeared in listings, producing cards that link nowhere and a broken url in the JSON-LD.

filterRoutablePosts drops them inside filterPosts, ahead of every other filter and of slicePosts, so count still yields count renderable posts.

The guard checks the type as well as the value, which is load-bearing: records come from getRecordsByPath, which casts the CMS JSON without validating it, so slug is a string only by convention. A non-string slug would make slug.trim() throw inside handlePosts — and the try/catch in BlogpostList turns a throw into logger.error + return null, so one malformed record would blank out the entire listing instead of just dropping itself. Worse than the bug being fixed.

Cases checked against the real handlePosts (only the valid post survives, and an all-invalid input returns null, matching the existing empty-list contract):

slug result
undefined dropped
"" dropped
null dropped
" " dropped
123 dropped, no throw
all posts invalid null

Compatibility

⚠️ Behavior fix, not an API change. The exported signatures of sortPosts and handlePosts are unchanged, and SortBy / VALID_SORT_ORDERS are untouched.

Two visible changes for live sites, both intended:

  • Listings will actually re-sort. Posts with full ISO timestamps were previously frozen in records order and will now move to their correct date position. Sites relying on the broken order will see their listings change.
  • Posts without a slug disappear from listings. They were never reachable, but they did occupy slots.

Worth a note in the release notes.

Checks

deno task check (the pre-commit hook) passes on every commit: fmt over 2099 files, lint over 1979, and deno check on every mod.ts.

Left out on purpose

  • blog/sections/Template.tsx:74 has the same string-concat pattern, but for display — with a full ISO timestamp it renders "Invalid Date". Same root cause, different symptom; kept out to keep this PR scoped.
  • No tiebreak for equal dates: migrated posts frequently share a date, and those stay in whatever order the records returned.
  • BlogPostPage and BlogPostItem both do posts.find((post) => post.slug === slug). If the route param is ever undefined, that matches a slug-less record; those loaders don't go through handlePosts, so this PR doesn't cover them.
  • The localeCompare branch has its inversion backwards for string fields (title_asc returns Z→A). Pre-existing, out of scope, and sites are presumably compensating — but worth a separate issue.

🤖 Generated with Claude Code

The date comparator built a Date by concatenating a time onto post.date,
which assumes the value is exactly YYYY-MM-DD. BlogPost.date is a plain
string and the CMS also stores full ISO timestamps, so
"2026-08-05T12:51:59Z" became "2026-08-05T12:51:59ZT00:00:00" -> Invalid
Date -> getTime() is NaN.

A NaN comparator result is treated as +0 and toSorted is stable, so a post
with a full timestamp never moved -- it stayed wherever the records put it.
Listings with sortBy "date_desc" showed yesterday's post above today's.

Only append a time when the value is a bare date, and use T00:00:00Z so a
bare date is UTC midnight instead of local midnight -- ordering no longer
depends on the server timezone. Unparseable values fall back to 0 so NaN
never reaches the comparator.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.160.1 update
  • 🎉 for Minor 0.161.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change updates blog post date parsing in handlePosts. It adds a helper that normalizes bare dates to UTC, uses that helper during sorting, and stores the awaited sorted posts in a local variable before returning them.

Changes

Blog date sorting

Layer / File(s) Summary
Date normalization and sorting
blog/core/handlePosts.ts
Adds dateToTime to parse bare dates as UTC midnight, accept ISO timestamps, and return 0 for invalid values. The date comparison now uses this helper. handlePosts also assigns the awaited sorted posts to sorted before it returns them.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • deco-cx/apps#1572: Both PRs change blog/core/handlePosts.ts date sorting by normalizing bare date strings to UTC before sort comparison.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title partially relates to the changeset. It mentions date sorting with ISO timestamps, which is the first fix, but omits the second fix about dropping posts without a slug.
Description check ✅ Passed The description comprehensively explains both fixes with technical detail, root causes, and verification results. Required sections from the template are missing (Issue Link, Loom Video, Demonstration Link), but the core contribution is well documented.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-blog-date-sorting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A post whose slug is missing, empty or blank has no route. It still rendered
in listings, producing cards that link nowhere and a broken url in the
JSON-LD.

Filter it out in filterPosts, ahead of every other filter and of slicePosts,
so `count` still yields `count` renderable posts. Records come straight from
the CMS and are cast without validation, so the guard also checks the type:
a non-string slug would otherwise throw inside handlePosts, and the
try/catch in the loaders would turn that into an empty listing.

Co-Authored-By: Claude <noreply@anthropic.com>
@aka-sacci-ccr aka-sacci-ccr changed the title fix(blog): compare full ISO timestamps when sorting posts by date fix(blog): compare full ISO timestamps when sorting, and drop posts without a slug Aug 5, 2026

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread blog/core/handlePosts.ts
Normalizing only bare YYYY-MM-DD left a gap: per spec a date-only string is
parsed as UTC, but an ISO datetime with no timezone designator is parsed as
local time. So "2026-08-05T23:30:00" ordered differently against a bare
"2026-08-06" depending on the server timezone -- the two swap places in
America/Sao_Paulo but not in UTC or Asia/Tokyo.

Match any ISO date or datetime lacking a designator and append Z. Covers
minute precision and fractional seconds too; strings that already carry a Z
or a ±hh:mm offset are left untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
@aka-sacci-ccr
aka-sacci-ccr merged commit 7108b47 into main Aug 5, 2026
5 checks passed
@aka-sacci-ccr
aka-sacci-ccr deleted the fix-blog-date-sorting branch August 5, 2026 16:30
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.

1 participant