Skip to content

fix(navigation): make blog transitions reliable - #10

Merged
yororoA merged 1 commit into
mainfrom
feat/blog-resilience
Oct 10, 2026
Merged

yororoA merged 1 commit into
mainfrom
feat/blog-resilience

Conversation

@yororoA

@yororoA yororoA commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary by Sourcery

Make blog navigation transitions reliable with route loading feedback and robust client-navigation fallbacks.

New Features:

  • Add route-level loading states for blog and home pages.

Bug Fixes:

  • Make blog transitions resilient by preventing overlapping wipes and falling back to native navigation when client routing stalls or fails.
  • Restore route prefetching for blog links to improve navigation reliability.

Enhancements:

  • Use consistent cached server content fetching for article and moment detail pages.

@sourcery-ai

sourcery-ai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens blog transitions by coordinating the custom wipe with client-router commits, adding a guarded native-navigation fallback and route loading states, while restoring link prefetching and standardizing server content caching.

Sequence diagram for reliable blog route transitions

sequenceDiagram
    actor User
    participant P3RLink
    participant P3REffects
    participant ClientRouter
    participant BlogRoute
    participant Browser

    User->>P3RLink: click blog link
    P3RLink->>P3REffects: dispatch P3RWipeRequest
    P3REffects->>ClientRouter: router.push or router.replace
    P3REffects->>P3REffects: expand wipe and wait for commit
    ClientRouter-->>P3REffects: route commit
    P3REffects->>BlogRoute: reveal route
    BlogRoute-->>User: show route-loading while content loads
    alt client route stalls for 8000ms
        P3REffects->>Browser: location.assign(destination)
    end
Loading

Flow diagram for blog navigation fallback

flowchart TD
    A[Blog link clicked] --> B{P3R transition active?}
    B -- Yes --> C[Ignore duplicate navigation]
    B -- No --> D[Start wipe transition]
    D --> E{Client router commits?}
    E -- Yes --> F[Reveal destination route]
    E -- No after 8000ms --> G[Native location.assign]
    D --> H{Router navigation throws?}
    H -- Yes --> G
Loading

File-Level Changes

Change Details Files
Make the custom wipe transition complete reliably across client and server route changes.
  • Invoke client navigation before starting the animation and track router commit readiness.
  • Add an 8-second native-navigation fallback for stalled routes and clear failures safely.
  • Ignore overlapping transition requests and preserve Escape cancellation behavior.
src/components/appearance/p3r-effects.tsx
src/components/appearance/p3r-link.tsx
src/components/appearance/p3r-transition.ts
Add route-level loading UI for blog navigation states.
  • Reuse a client blog loading component for blog and home route segments.
  • Render localized loading status content with existing blog styles.
src/app/(blog)/loading.tsx
src/app/home/loading.tsx
src/components/blog/route-loading.tsx
Restore framework link prefetching for blog navigation.
  • Remove explicit prefetch disabling from archive, article, home, and calendar links.
src/components/blog/archive.tsx
src/components/blog/articles.tsx
src/components/blog/home.tsx
Use consistent revalidated server content fetching for detail pages.
  • Remove the fresh/no-store fetch option from the public content helper.
  • Fetch article and moment details through the shared revalidation and tagging policy.
src/lib/server/public-content.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ark Ready Ready Preview Oct 10, 2026 8:03am UTC

@yororoA
yororoA merged commit 7daea14 into main Oct 10, 2026
5 checks passed

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

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/components/appearance/p3r-effects.tsx" line_range="114" />
<code_context>
     const request = (event: Event) => {
       if (motion.matches || document.hidden || useRainConnection.getState().connection) return
       event.preventDefault()
+      if (active) return
       start((event as CustomEvent<P3RWipeRequest>).detail)
     }
</code_context>
<issue_to_address>
**Second destination never opens**

When a user activates another P3RLink while a wipe is active, `request` calls `preventDefault()` before checking `active`, then returns without starting the request; `P3RLink` treats it as accepted and cancels the link navigation, so the second destination never opens.

Check `active` before preventing the request, and let the link navigation proceed when a wipe is already active.
</issue_to_address>

### Comment 2
<location path="src/components/appearance/p3r-effects.tsx" line_range="49" />
<code_context>
-      navigate?.()
-    }
-    const start = ({ x, y, navigate }: P3RWipeRequest) => {
+    const skip = () => clear()
+    const start = ({ x, y, navigate, fallback }: P3RWipeRequest) => {
       clear()
</code_context>
<issue_to_address>
**Stalled route loses fallback**

When a client route has started and the user skips the wipe before it commits, `skip()` calls `clear()`, which cancels the route fallback timer; if the route does not commit, no native navigation occurs and the user remains on the old page.

Preserve or rearm the route fallback timer when skipping an in-flight navigation.
</issue_to_address>

### Comment 3
<location path="src/components/appearance/p3r-effects.tsx" line_range="90-94" />
<code_context>
-            // A failed route must never leave an undismissable visual layer.
-            timeout = window.setTimeout(clear, 12000)
+            // A stalled client route must still reach its destination.
+            timeout = window.setTimeout(() => {
+              committed.current = null
+              if (!fallback) { clear(); return }
+              try { fallback() } catch { clear() }
+            }, ROUTE_FALLBACK_MS)
           }
           if (ready && now - started >= 440) {
</code_context>
<issue_to_address>
**Committed route reloads**

When the pathname commits but animation frames remain stalled until the fallback timer fires, the timeout calls `fallback()` without checking `ready`, so `location.assign()` reloads the destination despite the completed client transition and can discard client-side state.

Skip the fallback when the route has already committed, and clear the timer on commit.
</issue_to_address>

### Comment 4
<location path="src/components/appearance/p3r-link.tsx" line_range="41" />
<code_context>
         return true
+      }, fallback: () => {
+        const destination = `${url.pathname}${url.search}${url.hash}`
+        location.assign(destination)
       } })
       if (accepted) event.preventDefault()
</code_context>
<issue_to_address>
**Replace adds history entry**

When a `replace` navigation reaches the native fallback after the client route fails to commit, the fallback calls `location.assign()` instead of replacing the current entry, so the destination is added to browser history and Back returns to the page that was meant to be replaced.

Use `location.replace()` for replace navigations and `location.assign()` for other navigations.
</issue_to_address>

### Comment 5
<location path="src/lib/server/public-content.ts" line_range="51" />
<code_context>

 export const getArticle = cache(async (id: string) => {
   try {
-    const result = await publicEnvelope<Entry>(`knowledge/${id}`, true)
+    const result = await publicEnvelope<Entry>(`knowledge/${id}`)
     return result.data
</code_context>
<issue_to_address>
**Like counts stay outdated**

When an article or moment receives a like while its detail response is cached, `getArticle` and `getMoment` cache detail payloads, including like counts, for up to 300 seconds, while like requests do not invalidate `public-blog-content`; other visitors therefore see outdated counts.

Invalidate `public-blog-content` after successful article and moment like requests.

Also at `src/lib/server/public-content.ts:48`, `src/lib/server/public-content.ts:86`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and the change alters client navigation behavior and changes article and moment detail fetches from uncached requests to revalidated caching. If incorrect, navigation may fall back to a full reload after a delay, and stale detail content could be served for the cache window; the latter outlives a revert but is bounded and repairable through revalidation or cache invalidation.

Blocking findings: src/components/appearance/p3r-effects.tsx:114, src/components/appearance/p3r-effects.tsx:49, src/components/appearance/p3r-effects.tsx:94, src/components/appearance/p3r-link.tsx:41, src/lib/server/public-content.ts:51


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

const request = (event: Event) => {
if (motion.matches || document.hidden || useRainConnection.getState().connection) return
event.preventDefault()
if (active) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Second destination never opens

When a user activates another P3RLink while a wipe is active, request calls preventDefault() before checking active, then returns without starting the request; P3RLink treats it as accepted and cancels the link navigation, so the second destination never opens.

Check active before preventing the request, and let the link navigation proceed when a wipe is already active.

Prompt for AI agents
In `src/components/appearance/p3r-effects.tsx` at line 114:

**Second destination never opens**

When a user activates another P3RLink while a wipe is active, `request` calls `preventDefault()` before checking `active`, then returns without starting the request; `P3RLink` treats it as accepted and cancels the link navigation, so the second destination never opens.

Check `active` before preventing the request, and let the link navigation proceed when a wipe is already active.

navigate?.()
}
const start = ({ x, y, navigate }: P3RWipeRequest) => {
const skip = () => clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Stalled route loses fallback

When a client route has started and the user skips the wipe before it commits, skip() calls clear(), which cancels the route fallback timer; if the route does not commit, no native navigation occurs and the user remains on the old page.

Preserve or rearm the route fallback timer when skipping an in-flight navigation.

Prompt for AI agents
In `src/components/appearance/p3r-effects.tsx` at line 49:

**Stalled route loses fallback**

When a client route has started and the user skips the wipe before it commits, `skip()` calls `clear()`, which cancels the route fallback timer; if the route does not commit, no native navigation occurs and the user remains on the old page.

Preserve or rearm the route fallback timer when skipping an in-flight navigation.

Comment on lines +90 to +94
timeout = window.setTimeout(() => {
committed.current = null
if (!fallback) { clear(); return }
try { fallback() } catch { clear() }
}, ROUTE_FALLBACK_MS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Committed route reloads

When the pathname commits but animation frames remain stalled until the fallback timer fires, the timeout calls fallback() without checking ready, so location.assign() reloads the destination despite the completed client transition and can discard client-side state.

Skip the fallback when the route has already committed, and clear the timer on commit.

Prompt for AI agents
In `src/components/appearance/p3r-effects.tsx` at lines 90-94:

**Committed route reloads**

When the pathname commits but animation frames remain stalled until the fallback timer fires, the timeout calls `fallback()` without checking `ready`, so `location.assign()` reloads the destination despite the completed client transition and can discard client-side state.

Skip the fallback when the route has already committed, and clear the timer on commit.

return true
}, fallback: () => {
const destination = `${url.pathname}${url.search}${url.hash}`
location.assign(destination)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Replace adds history entry

When a replace navigation reaches the native fallback after the client route fails to commit, the fallback calls location.assign() instead of replacing the current entry, so the destination is added to browser history and Back returns to the page that was meant to be replaced.

Use location.replace() for replace navigations and location.assign() for other navigations.

Prompt for AI agents
In `src/components/appearance/p3r-link.tsx` at line 41:

**Replace adds history entry**

When a `replace` navigation reaches the native fallback after the client route fails to commit, the fallback calls `location.assign()` instead of replacing the current entry, so the destination is added to browser history and Back returns to the page that was meant to be replaced.

Use `location.replace()` for replace navigations and `location.assign()` for other navigations.


export const getArticle = cache(async (id: string) => {
try {
const result = await publicEnvelope<Entry>(`knowledge/${id}`, true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Like counts stay outdated

When an article or moment receives a like while its detail response is cached, getArticle and getMoment cache detail payloads, including like counts, for up to 300 seconds, while like requests do not invalidate public-blog-content; other visitors therefore see outdated counts.

Invalidate public-blog-content after successful article and moment like requests.

Also at src/lib/server/public-content.ts:48, src/lib/server/public-content.ts:86.

Prompt for AI agents
In `src/lib/server/public-content.ts` at line 51:

**Like counts stay outdated**

When an article or moment receives a like while its detail response is cached, `getArticle` and `getMoment` cache detail payloads, including like counts, for up to 300 seconds, while like requests do not invalidate `public-blog-content`; other visitors therefore see outdated counts.

Invalidate `public-blog-content` after successful article and moment like requests.

Also at `src/lib/server/public-content.ts:48`, `src/lib/server/public-content.ts:86`.

This branch was successfully deployed

1 active deployment
Preview — 7a060ebc Deployed Oct 10, 2026 by vercel[bot]
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