Skip to content

Self-hosted: Reader theme with a split archive view - #1450

Merged
feruzm merged 3 commits into
developfrom
feature/self-hosted-reader-theme
Aug 12, 2026
Merged

Self-hosted: Reader theme with a split archive view#1450
feruzm merged 3 commits into
developfrom
feature/self-hosted-reader-theme

Conversation

@feruzm

@feruzm feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes #1421

What

The second layout-level design on the theme manifest architecture: a Reader template that keeps the post archive as a persistent rail beside the open article, so moving between posts never leaves the page.

How

  • Shell (themes/reader/reader-shell.tsx): a compact masthead strip (title, filters, search, user menu) over a two-pane split; both panes scroll independently. On small screens the split collapses to one pane at a time: the feed route shows the rail full-width and every other route (post, search, publish) shows the content pane.
  • Rail (reader-rail.tsx): every entry as a compact row with the open one highlighted, infinite paging, and j/k keyboard navigation to the next and previous entry (arrow keys are left alone so they keep scrolling the article). Deep links work unchanged; the rail just marks whatever permlink the route carries.
  • Home pane (reader-home.tsx): the feed route's ArchiveList seam becomes a greeting with the keyboard hint, since the rail owns the archive. Cards stay the shared default so search results keep their look.
  • Shared fetching: the seam default's feed queries are extracted into useArchiveFeed and consumed by both BlogPostsList and the rail, so the two cannot drift (same queries, same DMCA filtering path, same paging).
  • Registered across the roster, the /v1/templates display catalog, the editor label map, the CSS registry and the guard suites (roster lockstep, appearance tokens, failure states). sidebar and listType are declared unsupported so the editor hides them under this theme.

Tests

  • SPA: 916 passing (registry resolution for Reader, roster lockstep, token guards, failure-state sweep)
  • Hosting API: 419 passing (catalog derives from the roster)
  • Both typechecks and the SPA production build green

The second layout-level design on the theme manifests: the archive as a
persistent rail beside the open post, both panes scrolling independently,
j and k moving between entries without leaving the page. The feed route
renders the split shell with a greeting pane (the rail owns the archive),
article routes open with the rail alongside and deep links work unchanged.
On small screens the split collapses to one pane at a time: the feed route
is the rail, everything else shows the content pane.

Fetching for the rail is the seam default's own, extracted into a shared
useArchiveFeed hook so the two cannot drift. Cards stay the shared default
so search results keep their look inside the reading pane. The template
registers across the roster, the display catalog, the editor label map and
the CSS registry; the guard suites cover the new file and counts.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e092e09-4182-49c8-811d-5be0c2e7f217

📥 Commits

Reviewing files that changed from the base of the PR and between 7d65d7d and ce47b7e.

📒 Files selected for processing (17)
  • apps/self-hosted/hosting/api/src/style-template-display.ts
  • apps/self-hosted/hosting/api/src/style-templates.ts
  • apps/self-hosted/src/core/i18n-strings.ts
  • apps/self-hosted/src/features/blog/components/blog-posts-list.tsx
  • apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts
  • apps/self-hosted/src/features/floating-menu/config-fields.ts
  • apps/self-hosted/src/features/shared/failure-states.test.ts
  • apps/self-hosted/src/routes/$author.$permlink.tsx
  • apps/self-hosted/src/routes/$category.$author.$permlink.tsx
  • apps/self-hosted/src/styles/theme-appearance-tokens.test.ts
  • apps/self-hosted/src/styles/themes/index.css
  • apps/self-hosted/src/styles/themes/reader.css
  • apps/self-hosted/src/themes/reader/reader-home.tsx
  • apps/self-hosted/src/themes/reader/reader-rail.tsx
  • apps/self-hosted/src/themes/reader/reader-shell.tsx
  • apps/self-hosted/src/themes/registry.test.ts
  • apps/self-hosted/src/themes/registry.ts

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.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Self-hosted: Add Reader theme with split archive rail layout

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add Reader template with split shell and persistent archive rail beside articles.
• Share archive fetching via useArchiveFeed so rail and default feed stay identical.
• Register Reader across catalogs, labels, CSS tokens, and guard/test suites.
Diagram

graph TD
  roster["Style template roster"] --> catalog["Hosting API catalog"]
  roster --> registry["Theme registry"] --> shell["Reader shell"] --> rail["Archive rail"] --> hook["useArchiveFeed"] --> sdk{{"Ecency SDK queries"}}
  registry --> list["BlogPostsList seam"] --> hook
  subgraph Legend
    direction LR
    _mod["Module"] ~~~ _api["API surface"] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Implement rail as Sidebar + keep default Shell
  • ➕ Less bespoke layout code; reuses existing Shell/Navigation seams
  • ➕ Potentially fewer new responsive/scroll behaviors to maintain
  • ➖ Harder to guarantee independent scrolling + mobile “one-pane-at-a-time” behavior
  • ➖ Navigation/search/user menu placement is central to the Reader concept and may not fit the default shell constraints
2. Centralize j/k navigation in a global shortcut provider
  • ➕ Keyboard shortcuts become reusable for other themes/views
  • ➕ Avoids per-component window event listeners
  • ➖ More framework surface area and lifecycle complexity
  • ➖ Reader-only behavior may not justify additional global infrastructure

Recommendation: Current approach (dedicated Reader Shell + rail component) is the better fit because the feature is explicitly layout-level: independent pane scrolling, route-aware collapsing, and a masthead strip are difficult to express cleanly via existing seams. The shared useArchiveFeed hook is a strong choice to prevent query drift between the rail and the default feed.

Files changed (15) +628 / -59

Enhancement (5) +561 / -0
use-archive-feed.tsIntroduce useArchiveFeed shared infinite-query hook +67/-0

Introduce useArchiveFeed shared infinite-query hook

• Extracts archive feed query construction and selection/flattening into a reusable hook. Preserves distinct blog vs community query paths and the DMCA filtering behavior provided by the SDK ranked-posts query.

apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts

reader.cssAdd Reader theme appearance tokens (light + dark) +119/-0

Add Reader theme appearance tokens (light + dark)

• Defines the full --theme-* token contract for the Reader template, including typography split (sans UI, serif reading), porcelain/graphite palettes, accent contrast choices, and layout dimensions used by the shell.

apps/self-hosted/src/styles/themes/reader.css

reader-home.tsxAdd Reader home pane greeting (ArchiveList seam override) +49/-0

Add Reader home pane greeting (ArchiveList seam override)

• Implements a Reader-specific ArchiveList seam that renders a centered greeting and keyboard hint, since the archive itself lives in the rail. Pulls instance/community metadata for the displayed title and description.

apps/self-hosted/src/themes/reader/reader-home.tsx

reader-rail.tsxAdd Reader archive rail with infinite paging and j/k navigation +201/-0

Add Reader archive rail with infinite paging and j/k navigation

• Implements the persistent archive rail: compact entry rows, active entry highlighting, infinite paging, and error/empty/pending states aligned with existing query-outcome conventions. Adds global key handling for j/k navigation while avoiding input/textarea targets and keeping arrow-key scrolling intact.

apps/self-hosted/src/themes/reader/reader-rail.tsx

reader-shell.tsxAdd Reader split-pane shell with responsive collapse +125/-0

Add Reader split-pane shell with responsive collapse

• Introduces the Reader page frame with a compact masthead (title, filters, search, user menu) and a two-pane layout where both panes scroll independently. Collapses to one pane on small screens (feed route shows rail; other routes show content) and preserves access to post creation via the floating composer button.

apps/self-hosted/src/themes/reader/reader-shell.tsx

Refactor (1) +5 / -54
blog-posts-list.tsxRefactor BlogPostsList to use shared archive feed hook +5/-54

Refactor BlogPostsList to use shared archive feed hook

• Removes inline infinite-query setup and replaces it with useArchiveFeed(filter, limit). This keeps paging, enabled guards, and DMCA-safe community query logic aligned with other archive surfaces (e.g., the Reader rail).

apps/self-hosted/src/features/blog/components/blog-posts-list.tsx

Tests (3) +29 / -5
failure-states.test.tsGuard Reader rail emptiness state in failure-state suite +1/-0

Guard Reader rail emptiness state in failure-state suite

• Adds the Reader rail's no-posts claim to the guarded set to ensure empty/failure states remain covered as new surfaces are introduced.

apps/self-hosted/src/features/shared/failure-states.test.ts

theme-appearance-tokens.test.tsUpdate token guard counts for new Reader theme +4/-4

Update token guard counts for new Reader theme

• Updates expectations for the number of accent blocks and template blocks now that a seventh template exists. Ensures token coverage checks remain meaningful.

apps/self-hosted/src/styles/theme-appearance-tokens.test.ts

registry.test.tsExpand theme registry tests for Reader manifest resolution +24/-1

Expand theme registry tests for Reader manifest resolution

• Adds assertions that Reader provides Shell and ArchiveList overrides, defaults other seams, and declares unsupported options. Adjusts CSS-only template resolution logic to treat Reader as a layout-level theme alongside Journal.

apps/self-hosted/src/themes/registry.test.ts

Other (6) +33 / -0
style-template-display.tsExpose Reader template in hosting API display catalog +11/-0

Expose Reader template in hosting API display catalog

• Adds the 'reader' entry to STYLE_TEMPLATE_DISPLAY with name, tagline, palette, and heading style. This enables /v1/templates (and related consumers) to present Reader consistently.

apps/self-hosted/hosting/api/src/style-template-display.ts

style-templates.tsRegister 'reader' in StyleTemplate roster +1/-0

Register 'reader' in StyleTemplate roster

• Extends STYLE_TEMPLATES with the new 'reader' template id so it becomes a first-class selectable template across the system.

apps/self-hosted/hosting/api/src/style-templates.ts

i18n-strings.tsAdd Reader template label + Reader home hint strings +7/-0

Add Reader template label + Reader home hint strings

• Introduces translation keys for the Reader template option label and the Reader home-pane hint text (including the j/k tip). Updates the English translations accordingly.

apps/self-hosted/src/core/i18n-strings.ts

config-fields.tsAdd Reader template to configuration label map +1/-0

Add Reader template to configuration label map

• Maps the 'reader' template id to its translation key so the editor/config UI can render a human-friendly option label.

apps/self-hosted/src/features/floating-menu/config-fields.ts

index.cssInclude Reader theme stylesheet in global theme imports +1/-0

Include Reader theme stylesheet in global theme imports

• Adds an @import for reader.css so the Reader template's token contract is available at runtime.

apps/self-hosted/src/styles/themes/index.css

registry.tsRegister Reader manifest components and unsupported options +12/-0

Register Reader manifest components and unsupported options

• Adds ReaderHome and ReaderShell to the theme manifest registry under id 'reader'. Declares sidebar and listType unsupported so the editor hides inert options for this layout.

apps/self-hosted/src/themes/registry.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c5a90b13c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

author: `@${entryData.author}`,
permlink: entryData.permlink,
}}
search={{ raw: undefined }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the selected filter when opening a post

When a reader opens an entry from a non-default filter such as trending, this link carries only raw; the post route's validateSearch also retains only raw, so usePostsFilterState falls back to the first configured filter and ReaderRail immediately switches to a different feed. The supposedly persistent archive and subsequent j/k navigation therefore lose the selected filter as soon as a post opens; preserve that filter through post navigation or in shell state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in ce47b7e. Both post routes now retain a clamped optional filter search param and the rail carries a non-default filter into every post link and every j/k navigation, so the archive stays on the feed the reader was browsing. Canonical deep links stay unchanged (no filter injected when it is the default).


{entries.map((entry) => {
const entryData = entry.original_entry || entry;
const active = entry.permlink === activePermlink;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match the active row by its full effective post identity

When a community contains the same permlink from different authors, or an account-feed entry has an original_entry, this compares only the outer entry's permlink even though the link navigates using entryData.author and entryData.permlink. That can highlight the wrong or multiple rows, and the keyboard lookup uses the same incomplete identity; compare the route's author and permlink against the effective entry instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in ce47b7e. Active-row detection and the keyboard lookup now share one isOpen predicate comparing the effective entry (original_entry || entry) author AND permlink against the route params, so cross-posts highlight correctly and a permlink shared by two authors cannot mark both rows.


{/* The floating composer entry point the default navigation mounts; a
theme shell must never cost owners and community members the way in. */}
<CreatePostButton />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the internal publisher inside the Reader shell

When resolveCreatePostTarget selects the internal composer, this button navigates to /publish, but the inspected routes/publish.tsx renders PublishPageContent directly rather than through BlogLayout. Consequently ReaderShell unmounts, its masthead and content pane disappear, and the default sidebar returns despite Reader declaring sidebar options unsupported; route the publisher through the theme shell if it is meant to be one of Reader's content-pane routes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged, but this is pre-existing uniform topology rather than a Reader regression: routes/publish.tsx renders the composer directly for EVERY template (the default included; only /blog and /search go through BlogLayout), so no theme's shell wraps it and no sidebar returns there. Routing the composer and edit routes through the theme Shell seam is a deliberate follow-up decision for the seam architecture, tracked with the platform carry-alongs, since wrapping a full-screen editor in a split frame has its own tradeoffs.

…full identity

Post routes now retain a clamped optional filter search param and the rail
carries a non-default filter into every post link and j/k move, so opening
an entry no longer resets the persistent archive to the default feed.
Active-row detection and the keyboard lookup compare the effective entry's
author and permlink (original_entry for reblogs) against the route params,
so cross-posts highlight correctly and a shared permlink from two authors
cannot mark both rows.
@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (2)

Grey Divider


Action required

1. Reader rail selects wrong entry for cross-posts ✓ Resolved 🐞 Bug ≡ Correctness
Description
In reader-rail.tsx, the rail renders and navigates using the unwrapped entry.original_entry
permlink but determines the active row and current keyboard-navigation position using the wrapper
entry.permlink, so for cross-post/reblog items the open post is never detected. This prevents
active-row highlighting from landing on the open entry and makes j/k navigation behave as if
nothing is open (index -1), restarting from the top instead of moving relative to the current
post.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R81-91]

+      const current = list.findIndex(
+        (entry) => entry.permlink === activeRef.current,
+      );
+      // Nothing open yet: either key starts at the top.
+      const next =
+        current === -1
+          ? 0
+          : Math.min(Math.max(current + (forward ? 1 : -1), 0), list.length - 1);
+      if (next === current) return;
+
+      const target = list[next].original_entry || list[next];
Relevance

●●● Strong

Clear functional bug (active row + j/k nav) and aligns with PR intent; likely to be fixed.

PR-#690

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited code paths show an inconsistent identity source: the j/k navigation target (line 91)
and the rendered Link destination (lines 134–139) both derive the permlink from
entry.original_entry || entry, and the row logic similarly unwraps for permlink usage around line
128, but the lookup for the current index (findIndex at line 82) and the active flag used for
highlighting (line 129) compare the route’s activePermlink against the raw wrapper
entry.permlink without unwrapping. Because the SDK preserves a cross-post wrapper while attaching
the fetched original as original_entry, these permlinks can differ for resolved cross-posts, so
the active match fails and keyboard navigation treats the state as “nothing open yet.”

apps/self-hosted/src/themes/reader/reader-rail.tsx[81-96]
apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]
apps/self-hosted/src/themes/reader/reader-rail.tsx[79-97]
packages/sdk/src/modules/bridge/requests.ts[23-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Reader archive rail (`apps/self-hosted/src/themes/reader/reader-rail.tsx`) mixes identities for cross-post/reblog entries: it renders and navigates using the unwrapped `entry.original_entry || entry` permlink, but computes the active row and keyboard-navigation current index using the wrapper `entry.permlink`. This causes the currently open cross-post to never be recognized, so the active highlight is missing and `j`/`k` navigation restarts from the top (as if index `-1`) instead of moving relative to the actually open post.
## Issue Context
- The SDK keeps the cross-post “wrapper” entry and attaches the resolved original post as `original_entry`, so the wrapper `entry.permlink` can differ from `entry.original_entry.permlink`.
- The rail’s rendered `Link` navigates using `entryData.permlink` where `entryData = entry.original_entry || entry` (unwrapped identity).
- The `j`/`k` handler similarly computes its navigation destination from `list[next].original_entry || list[next]`.
- However, the `findIndex` used to determine the current position for `j`/`k`, and the `active` boolean used to highlight the open row, compare the route’s active permlink against the raw wrapper `entry.permlink`.
- Consider also comparing both author and permlink when determining “active/current,” since permlinks are not globally unique.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[79-91]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reader rail selects wrong entry for cross-posts 🐞 Bug ≡ Correctness
Description
In reader-rail.tsx, the rail renders and navigates using the unwrapped entry.original_entry
permlink but determines the active row and current keyboard-navigation position using the wrapper
entry.permlink, so for cross-post/reblog items the open post is never detected. This prevents
active-row highlighting from landing on the open entry and makes j/k navigation behave as if
nothing is open (index -1), restarting from the top instead of moving relative to the current
post.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R81-91]

+      const current = list.findIndex(
+        (entry) => entry.permlink === activeRef.current,
+      );
+      // Nothing open yet: either key starts at the top.
+      const next =
+        current === -1
+          ? 0
+          : Math.min(Math.max(current + (forward ? 1 : -1), 0), list.length - 1);
+      if (next === current) return;
+
+      const target = list[next].original_entry || list[next];
Relevance

●●● Strong

Clear functional bug (active row + j/k nav) and aligns with PR intent; likely to be fixed.

PR-#690

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited code paths show an inconsistent identity source: the j/k navigation target (line 91)
and the rendered Link destination (lines 134–139) both derive the permlink from
entry.original_entry || entry, and the row logic similarly unwraps for permlink usage around line
128, but the lookup for the current index (findIndex at line 82) and the active flag used for
highlighting (line 129) compare the route’s activePermlink against the raw wrapper
entry.permlink without unwrapping. Because the SDK preserves a cross-post wrapper while attaching
the fetched original as original_entry, these permlinks can differ for resolved cross-posts, so
the active match fails and keyboard navigation treats the state as “nothing open yet.”

apps/self-hosted/src/themes/reader/reader-rail.tsx[81-96]
apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]
apps/self-hosted/src/themes/reader/reader-rail.tsx[79-97]
packages/sdk/src/modules/bridge/requests.ts[23-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Reader archive rail (`apps/self-hosted/src/themes/reader/reader-rail.tsx`) mixes identities for cross-post/reblog entries: it renders and navigates using the unwrapped `entry.original_entry || entry` permlink, but computes the active row and keyboard-navigation current index using the wrapper `entry.permlink`. This causes the currently open cross-post to never be recognized, so the active highlight is missing and `j`/`k` navigation restarts from the top (as if index `-1`) instead of moving relative to the actually open post.
## Issue Context
- The SDK keeps the cross-post “wrapper” entry and attaches the resolved original post as `original_entry`, so the wrapper `entry.permlink` can differ from `entry.original_entry.permlink`.
- The rail’s rendered `Link` navigates using `entryData.permlink` where `entryData = entry.original_entry || entry` (unwrapped identity).
- The `j`/`k` handler similarly computes its navigation destination from `list[next].original_entry || list[next]`.
- However, the `findIndex` used to determine the current position for `j`/`k`, and the `active` boolean used to highlight the open row, compare the route’s active permlink against the raw wrapper `entry.permlink`.
- Consider also comparing both author and permlink when determining “active/current,” since permlinks are not globally unique.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[79-91]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Post navigation drops filter 🐞 Bug ≡ Correctness
Description
ReaderRail omits the active filter when navigating, so opening a post from any non-default archive
immediately resets the rail to the first configured filter. Both j/k navigation and row links
construct destinations without the filter.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R93-96]

+      navigate({
+        to: '/$author/$permlink',
+        params: { author: `@${target.author}`, permlink: target.permlink },
+        search: { raw: undefined },
Evidence
ReaderShell passes usePostsFilterState().currentFilter into the rail, while that hook defaults to
the first configured filter whenever location.search.filter is absent. The added keyboard
navigation and Link both supply only raw, and both post route validators currently retain only
raw, so a selected non-default filter cannot survive navigation.

apps/self-hosted/src/themes/reader/reader-shell.tsx[44-45]
apps/self-hosted/src/themes/reader/reader-shell.tsx[99-105]
apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts[16-30]
apps/self-hosted/src/themes/reader/reader-rail.tsx[91-97]
apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
apps/self-hosted/src/routes/$author.$permlink.tsx[4-9]
apps/self-hosted/hosting/default-config.json[50-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reader post navigation drops the selected archive filter, causing the rail to switch back to the configured default after opening a post.
## Issue Context
`ReaderShell` derives the rail filter from the current route search. Preserve and validate the filter on both keyboard and mouse navigation to post routes.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[37-53]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[91-97]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
- apps/self-hosted/src/routes/$author.$permlink.tsx[4-9]
- apps/self-hosted/src/routes/$category.$author.$permlink.tsx[4-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. useArchiveFeed uses any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new useArchiveFeed hook introduces an explicit any type in its selectPosts callback, which
violates the project rule forbidding any in new/changed TypeScript code. This weakens type-safety
and can mask incorrect query result shapes.
Code

apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[R39-42]

+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const selectPosts = useCallback(
+    (data: { pages: any[][] }) => data.pages.flat(),
+    []
Relevance

●●● Strong

Team recently accepted removing newly introduced any/no-explicit-any escapes; likely will require
proper typing here.

PR-#1443

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new hook
explicitly types pages as any[][] (and suppresses the lint rule), which is a direct violation.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useArchiveFeed` introduces an explicit `any` type in the `selectPosts` callback (`(data: { pages: any[][] }) => ...`). The compliance rule disallows `any` in new/modified TypeScript.
## Issue Context
This hook is shared by both the default blog list and the Reader rail; keeping its query result types accurate is important to prevent silent runtime shape drift.
## Fix Focus Areas
- apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Selected filter is discarded ✓ Resolved 🐞 Bug ≡ Correctness
Description
ReaderRail post links omit the active filter, so opening a post from /blog?filter=hot makes
ReaderShell fall back to the first configured feed and replaces the persistent rail. The selected
post may consequently disappear from the rail.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[139]

+            search={{ raw: undefined }}
Relevance

●●● Strong

They’ve accepted fixes preserving/setting route search state; likely will keep filter when
navigating from rail.

PR-#690
PR-#1441

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rail passes only raw when navigating, while ReaderShell obtains its feed from
usePostsFilterState; that hook defaults to the first configured filter whenever the current route
search has no filter. Both post route validators currently retain only raw.

apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
apps/self-hosted/src/themes/reader/reader-shell.tsx[44-45]
apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts[16-30]
apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reader post navigation drops the active archive filter, causing the rail to switch back to the configured default feed.
## Issue Context
Preserve and validate the Reader filter on post routes and links, or maintain Reader filter state independently of route search. Apply the same behavior to click and keyboard navigation.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[134-140]
- apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]
- apps/self-hosted/src/routes/$category.$author.$permlink.tsx[6-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. useArchiveFeed uses any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new useArchiveFeed hook introduces an explicit any type in its selectPosts callback, which
violates the project rule forbidding any in new/changed TypeScript code. This weakens type-safety
and can mask incorrect query result shapes.
Code

apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[R39-42]

+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const selectPosts = useCallback(
+    (data: { pages: any[][] }) => data.pages.flat(),
+    []
Relevance

●●● Strong

Team recently accepted removing newly introduced any/no-explicit-any escapes; likely will require
proper typing here.

PR-#1443

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new hook
explicitly types pages as any[][] (and suppresses the lint rule), which is a direct violation.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useArchiveFeed` introduces an explicit `any` type in the `selectPosts` callback (`(data: { pages: any[][] }) => ...`). The compliance rule disallows `any` in new/modified TypeScript.
## Issue Context
This hook is shared by both the default blog list and the Reader rail; keeping its query result types accurate is important to prevent silent runtime shape drift.
## Fix Focus Areas
- apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (5)
7. Selected filter is discarded 🐞 Bug ≡ Correctness
Description
ReaderRail post links omit the active filter, so opening a post from /blog?filter=hot makes
ReaderShell fall back to the first configured feed and replaces the persistent rail. The selected
post may consequently disappear from the rail.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[139]

+            search={{ raw: undefined }}
Relevance

●●● Strong

They’ve accepted fixes preserving/setting route search state; likely will keep filter when
navigating from rail.

PR-#690
PR-#1441

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rail passes only raw when navigating, while ReaderShell obtains its feed from
usePostsFilterState; that hook defaults to the first configured filter whenever the current route
search has no filter. Both post route validators currently retain only raw.

apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
apps/self-hosted/src/themes/reader/reader-shell.tsx[44-45]
apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts[16-30]
apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reader post navigation drops the active archive filter, causing the rail to switch back to the configured default feed.
## Issue Context
Preserve and validate the Reader filter on post routes and links, or maintain Reader filter state independently of route search. Apply the same behavior to click and keyboard navigation.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[134-140]
- apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]
- apps/self-hosted/src/routes/$category.$author.$permlink.tsx[6-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Publish drops Reader shell ✗ Dismissed 🐞 Bug ≡ Correctness
Description
ReaderShell's CreatePostButton links to /publish, but that route renders a standalone layout
instead of resolving the theme Shell. Publishing therefore removes the Reader masthead and split
rail and renders the legacy sidebar layout despite the new shell explicitly treating publish as a
content-pane route.
Code

apps/self-hosted/src/themes/reader/reader-shell.tsx[R120-122]

+      {/* The floating composer entry point the default navigation mounts; a
+          theme shell must never cost owners and community members the way in. */}
+      <CreatePostButton />
Relevance

●● Moderate

Publish-in-shell behavior is architectural/theme-routing; no close precedent, though they care about
publish entry points.

PR-#1104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added ReaderShell exposes CreatePostButton, whose internal target is /publish. The publish
route directly returns its own min-h-screen grid and BlogSidebar, whereas BlogLayout is the
component that resolves and mounts the active theme Shell.

apps/self-hosted/src/themes/reader/reader-shell.tsx[14-22]
apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[52-56]
apps/self-hosted/src/routes/publish.tsx[37-61]
apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The publish entry point exits the Reader shell because the publish route bypasses BlogLayout/theme component resolution.
## Issue Context
Keep the existing publish authorization gate, but route authorized publish content through the Reader-aware shell or add an equivalent theme-aware route wrapper.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
- apps/self-hosted/src/routes/publish.tsx[37-61]
- apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Selection ignores post author 🐞 Bug ≡ Correctness
Description
ReaderRail determines the active row and j/k keyboard navigation target by matching only the outer
wrapper entry’s permlink, even though post identity is defined by author+permlink and cross-posts
resolve via original_entry. This can cause multiple rows to highlight when different authors share
a permlink in mixed feeds and can also prevent cross-post rows from becoming active because
navigation targets the resolved entry while selection compares the unresolved wrapper.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R81-83]

+      const current = list.findIndex(
+        (entry) => entry.permlink === activeRef.current,
+      );
Evidence
The route/BlogPostPage identify posts using both author and permlink, but ReaderRail only extracts
params.permlink (apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]) and then matches
entries solely by permlink in both the active-row check (entry.permlink === activePermlink,
apps/self-hosted/src/themes/reader/reader-rail.tsx[129]) and the keyboard handler’s index lookup
(list.findIndex((entry) => entry.permlink === activeRef.current),
apps/self-hosted/src/themes/reader/reader-rail.tsx[81-83]). Because the feed can be
community-wide/mixed (via useArchiveFeed and ranked community queries), permlink collisions across
different authors are plausible, making multiple entries appear “active” and shifting the navigation
index. Additionally, the SDK represents cross-post destinations in original_entry, and ReaderRail
navigates to that resolved destination while its active/highlight comparisons use the unresolved
outer entry, demonstrating an identity mismatch between navigation and selection.

apps/self-hosted/src/routes/$author.$permlink.tsx[4-5]
apps/self-hosted/src/features/blog/components/blog-post-page.tsx[31-42]
apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]
apps/self-hosted/src/themes/reader/reader-rail.tsx[79-96]
apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]
packages/sdk/src/modules/bridge/requests.ts[23-36]
apps/self-hosted/src/themes/reader/reader-rail.tsx[81-83]
apps/self-hosted/src/themes/reader/reader-rail.tsx[129]
apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[47-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ReaderRail’s notion of “currently open/active” post is inconsistent and under-specified: it matches entries using only the wrapper entry’s `permlink`, but the app’s post identity requires an author+permlink pair, and cross-post rows should be keyed off the resolved `original_entry` identity. This leads to incorrect highlighting and j/k navigation when different authors share a permlink in mixed feeds, and can break activation for cross-post rows when navigation targets `original_entry` but selection compares the wrapper entry.
## Issue Context
- `ReaderRail` reads only `params.permlink` from the route and uses it as the active identifier.
- The rail compares `entry.permlink` against that value both for the active-row highlight and for locating the current index for keyboard navigation.
- In community mode / ranked feeds, entries from multiple authors are mixed, so permlink-only matching is not unique.
- Cross-post entries store the resolved destination in `entry.original_entry`; navigation should target the same canonical identity that highlight/keyboard lookup/ref tracking uses.
Implement a single canonical identity helper that uses `entry.original_entry || entry`, and derive a normalized route identity (including `params.author`, with any leading `@` stripped as done elsewhere, e.g. blog-post-page.tsx). Use the same canonical author+permlink pair for: computing active/highlight state, computing the keyboard navigation “current index”, maintaining refs, and performing navigation so all these behaviors stay consistent.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[37-40]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[63-68]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[79-96]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. j/k listener active while rail hidden on mobile 🐞 Bug ☼ Reliability
Description
ReaderRail is always mounted inside ReaderShell's <aside>, which is only CSS-hidden ('hidden
lg:block') on non-feed routes at mobile widths; ReaderRail's window keydown listener for 'j'/'k'
stays registered regardless, so pressing those keys on a mobile post/search/publish page silently
navigates away even though the rail providing that context is not visible.
Code

apps/self-hosted/src/themes/reader/reader-shell.tsx[R99-106]

+        <aside
+          className={clsx(
+            'w-full lg:w-[var(--theme-sidebar-width)] lg:shrink-0 lg:border-r border-theme overflow-y-auto bg-theme-secondary',
+            !isFeedRoute && 'hidden lg:block',
+          )}
+        >
+          <ReaderRail filter={currentFilter} />
+        </aside>
Evidence
reader-shell.tsx hides the <aside> containing ReaderRail purely via Tailwind's 'hidden lg:block'
utility on non-feed routes, but does not unmount it. reader-rail.tsx's useEffect registers a
window-level keydown handler for 'j'/'k' with no dependency on visibility or route, and the handler
unconditionally calls navigate() once a next entry is found, so it will fire and redirect the page
even while the rail is invisible on small screens.

apps/self-hosted/src/themes/reader/reader-shell.tsx[99-106]
apps/self-hosted/src/themes/reader/reader-rail.tsx[70-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Reader rail's keyboard navigation (j/k) listener stays active on `window` even when the rail's `<aside>` is CSS-hidden on mobile for non-feed routes, causing unexpected navigation when a user presses j/k on a mobile post/search/publish page.
## Issue Context
`ReaderShell` always renders `<ReaderRail>` inside an `<aside>` that is hidden via `hidden lg:block` (not unmounted) whenever the route isn't the feed route and the viewport is below the `lg` breakpoint. `ReaderRail`'s `useEffect` attaches a `keydown` listener unconditionally.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-shell.tsx[98-106]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[70-102]
Either pass a visibility flag down (e.g. based on `isFeedRoute` or a media query) that gates whether the keydown listener is attached, or avoid rendering `ReaderRail` at all when it is not visible on small screens, mounting it only when the aside is actually shown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Publish drops Reader shell 🐞 Bug ≡ Correctness
Description
ReaderShell's CreatePostButton links to /publish, but that route renders a standalone layout
instead of resolving the theme Shell. Publishing therefore removes the Reader masthead and split
rail and renders the legacy sidebar layout despite the new shell explicitly treating publish as a
content-pane route.
Code

apps/self-hosted/src/themes/reader/reader-shell.tsx[R120-122]

+      {/* The floating composer entry point the default navigation mounts; a
+          theme shell must never cost owners and community members the way in. */}
+      <CreatePostButton />
Relevance

●● Moderate

Publish-in-shell behavior is architectural/theme-routing; no close precedent, though they care about
publish entry points.

PR-#1104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added ReaderShell exposes CreatePostButton, whose internal target is /publish. The publish
route directly returns its own min-h-screen grid and BlogSidebar, whereas BlogLayout is the
component that resolves and mounts the active theme Shell.

apps/self-hosted/src/themes/reader/reader-shell.tsx[14-22]
apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[52-56]
apps/self-hosted/src/routes/publish.tsx[37-61]
apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The publish entry point exits the Reader shell because the publish route bypasses BlogLayout/theme component resolution.
## Issue Context
Keep the existing publish authorization gate, but route authorized publish content through the Reader-aware shell or add an equivalent theme-aware route wrapper.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
- apps/self-hosted/src/routes/publish.tsx[37-61]
- apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

12. Test file wrong location 📜 Skill insight ⌂ Architecture
Description
This PR modifies a test file that is co-located under src/features/... instead of the required
src/specs/... mapping, and it uses a .test.ts filename instead of the required
.spec.ts/.spec.tsx pattern. This breaks the project’s mandated test organization conventions.
Code

apps/self-hosted/src/features/shared/failure-states.test.ts[48]

+  'src/themes/reader/reader-rail.tsx:noPosts': 1,
Relevance

● Weak

Repo repeatedly rejected moving/renaming co-located *.test.ts to src/specs/*.spec.ts despite
policy claims.

PR-#1442
PR-#1443
PR-#1437

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668281 requires feature tests to live under the corresponding src/specs/...
subtree (not under src/features/...), and PR Compliance ID 2668286 requires .spec.ts/.spec.tsx
naming. The modified file is apps/self-hosted/src/features/shared/failure-states.test.ts and the
change occurs within it, confirming the test remains co-located and named .test.ts.

apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]
Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A modified test file is not in the required `src/specs/...` directory and does not follow the `.spec.*` naming convention.
## Issue Context
The checklist requires tests to be placed under `src/specs/...` (not co-located with source) and named using `.spec.ts` / `.spec.tsx`. This PR adds a new guarded-claim entry in `apps/self-hosted/src/features/shared/failure-states.test.ts`, keeping the file in a non-compliant location and name.
## Fix Focus Areas
- apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Test file wrong location 📜 Skill insight ⌂ Architecture
Description
This PR modifies a test file that is co-located under src/features/... instead of the required
src/specs/... mapping, and it uses a .test.ts filename instead of the required
.spec.ts/.spec.tsx pattern. This breaks the project’s mandated test organization conventions.
Code

apps/self-hosted/src/features/shared/failure-states.test.ts[48]

+  'src/themes/reader/reader-rail.tsx:noPosts': 1,
Relevance

● Weak

Repo repeatedly rejected moving/renaming co-located *.test.ts to src/specs/*.spec.ts despite
policy claims.

PR-#1442
PR-#1443
PR-#1437

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668281 requires feature tests to live under the corresponding src/specs/...
subtree (not under src/features/...), and PR Compliance ID 2668286 requires .spec.ts/.spec.tsx
naming. The modified file is apps/self-hosted/src/features/shared/failure-states.test.ts and the
change occurs within it, confirming the test remains co-located and named .test.ts.

apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]
Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A modified test file is not in the required `src/specs/...` directory and does not follow the `.spec.*` naming convention.
## Issue Context
The checklist requires tests to be placed under `src/specs/...` (not co-located with source) and named using `.spec.ts` / `.spec.tsx`. This PR adds a new guarded-claim entry in `apps/self-hosted/src/features/shared/failure-states.test.ts`, keeping the file in a non-compliant location and name.
## Fix Focus Areas
- apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts Outdated
Comment thread apps/self-hosted/src/themes/reader/reader-rail.tsx Outdated
Comment thread apps/self-hosted/src/themes/reader/reader-shell.tsx
Comment thread apps/self-hosted/src/themes/reader/reader-rail.tsx Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (1)

Grey Divider


Action required

1. Post navigation drops filter 🐞 Bug ≡ Correctness ⭐ New
Description
ReaderRail omits the active filter when navigating, so opening a post from any non-default archive
immediately resets the rail to the first configured filter. Both j/k navigation and row links
construct destinations without the filter.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R93-96]

+      navigate({
+        to: '/$author/$permlink',
+        params: { author: `@${target.author}`, permlink: target.permlink },
+        search: { raw: undefined },
Evidence
ReaderShell passes usePostsFilterState().currentFilter into the rail, while that hook defaults to
the first configured filter whenever location.search.filter is absent. The added keyboard
navigation and Link both supply only raw, and both post route validators currently retain only
raw, so a selected non-default filter cannot survive navigation.

apps/self-hosted/src/themes/reader/reader-shell.tsx[44-45]
apps/self-hosted/src/themes/reader/reader-shell.tsx[99-105]
apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts[16-30]
apps/self-hosted/src/themes/reader/reader-rail.tsx[91-97]
apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
apps/self-hosted/src/routes/$author.$permlink.tsx[4-9]
apps/self-hosted/hosting/default-config.json[50-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reader post navigation drops the selected archive filter, causing the rail to switch back to the configured default after opening a post.

## Issue Context
`ReaderShell` derives the rail filter from the current route search. Preserve and validate the filter on both keyboard and mouse navigation to post routes.

## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[37-53]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[91-97]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
- apps/self-hosted/src/routes/$author.$permlink.tsx[4-9]
- apps/self-hosted/src/routes/$category.$author.$permlink.tsx[4-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reader rail selects wrong entry for cross-posts 🐞 Bug ≡ Correctness
Description
In reader-rail.tsx, the rail renders and navigates using the unwrapped entry.original_entry
permlink but determines the active row and current keyboard-navigation position using the wrapper
entry.permlink, so for cross-post/reblog items the open post is never detected. This prevents
active-row highlighting from landing on the open entry and makes j/k navigation behave as if
nothing is open (index -1), restarting from the top instead of moving relative to the current
post.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R81-91]

+      const current = list.findIndex(
+        (entry) => entry.permlink === activeRef.current,
+      );
+      // Nothing open yet: either key starts at the top.
+      const next =
+        current === -1
+          ? 0
+          : Math.min(Math.max(current + (forward ? 1 : -1), 0), list.length - 1);
+      if (next === current) return;
+
+      const target = list[next].original_entry || list[next];
Relevance

●●● Strong

Clear functional bug (active row + j/k nav) and aligns with PR intent; likely to be fixed.

PR-#690

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited code paths show an inconsistent identity source: the j/k navigation target (line 91)
and the rendered Link destination (lines 134–139) both derive the permlink from
entry.original_entry || entry, and the row logic similarly unwraps for permlink usage around line
128, but the lookup for the current index (findIndex at line 82) and the active flag used for
highlighting (line 129) compare the route’s activePermlink against the raw wrapper
entry.permlink without unwrapping. Because the SDK preserves a cross-post wrapper while attaching
the fetched original as original_entry, these permlinks can differ for resolved cross-posts, so
the active match fails and keyboard navigation treats the state as “nothing open yet.”

apps/self-hosted/src/themes/reader/reader-rail.tsx[81-96]
apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]
apps/self-hosted/src/themes/reader/reader-rail.tsx[79-97]
packages/sdk/src/modules/bridge/requests.ts[23-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Reader archive rail (`apps/self-hosted/src/themes/reader/reader-rail.tsx`) mixes identities for cross-post/reblog entries: it renders and navigates using the unwrapped `entry.original_entry || entry` permlink, but computes the active row and keyboard-navigation current index using the wrapper `entry.permlink`. This causes the currently open cross-post to never be recognized, so the active highlight is missing and `j`/`k` navigation restarts from the top (as if index `-1`) instead of moving relative to the actually open post.
## Issue Context
- The SDK keeps the cross-post “wrapper” entry and attaches the resolved original post as `original_entry`, so the wrapper `entry.permlink` can differ from `entry.original_entry.permlink`.
- The rail’s rendered `Link` navigates using `entryData.permlink` where `entryData = entry.original_entry || entry` (unwrapped identity).
- The `j`/`k` handler similarly computes its navigation destination from `list[next].original_entry || list[next]`.
- However, the `findIndex` used to determine the current position for `j`/`k`, and the `active` boolean used to highlight the open row, compare the route’s active permlink against the raw wrapper `entry.permlink`.
- Consider also comparing both author and permlink when determining “active/current,” since permlinks are not globally unique.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[79-91]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Selection ignores post author 🐞 Bug ≡ Correctness ⭐ New
Description
ReaderRail determines the active row and j/k keyboard navigation target by matching only the outer
wrapper entry’s permlink, even though post identity is defined by author+permlink and cross-posts
resolve via original_entry. This can cause multiple rows to highlight when different authors share
a permlink in mixed feeds and can also prevent cross-post rows from becoming active because
navigation targets the resolved entry while selection compares the unresolved wrapper.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[R81-83]

+      const current = list.findIndex(
+        (entry) => entry.permlink === activeRef.current,
+      );
Evidence
The route/BlogPostPage identify posts using both author and permlink, but ReaderRail only extracts
params.permlink (apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]) and then matches
entries solely by permlink in both the active-row check (entry.permlink === activePermlink,
apps/self-hosted/src/themes/reader/reader-rail.tsx[129]) and the keyboard handler’s index lookup
(list.findIndex((entry) => entry.permlink === activeRef.current),
apps/self-hosted/src/themes/reader/reader-rail.tsx[81-83]). Because the feed can be
community-wide/mixed (via useArchiveFeed and ranked community queries), permlink collisions across
different authors are plausible, making multiple entries appear “active” and shifting the navigation
index. Additionally, the SDK represents cross-post destinations in original_entry, and ReaderRail
navigates to that resolved destination while its active/highlight comparisons use the unresolved
outer entry, demonstrating an identity mismatch between navigation and selection.

apps/self-hosted/src/routes/$author.$permlink.tsx[4-5]
apps/self-hosted/src/features/blog/components/blog-post-page.tsx[31-42]
apps/self-hosted/src/themes/reader/reader-rail.tsx[38-39]
apps/self-hosted/src/themes/reader/reader-rail.tsx[79-96]
apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]
packages/sdk/src/modules/bridge/requests.ts[23-36]
apps/self-hosted/src/themes/reader/reader-rail.tsx[81-83]
apps/self-hosted/src/themes/reader/reader-rail.tsx[129]
apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[47-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ReaderRail’s notion of “currently open/active” post is inconsistent and under-specified: it matches entries using only the wrapper entry’s `permlink`, but the app’s post identity requires an author+permlink pair, and cross-post rows should be keyed off the resolved `original_entry` identity. This leads to incorrect highlighting and j/k navigation when different authors share a permlink in mixed feeds, and can break activation for cross-post rows when navigation targets `original_entry` but selection compares the wrapper entry.

## Issue Context
- `ReaderRail` reads only `params.permlink` from the route and uses it as the active identifier.
- The rail compares `entry.permlink` against that value both for the active-row highlight and for locating the current index for keyboard navigation.
- In community mode / ranked feeds, entries from multiple authors are mixed, so permlink-only matching is not unique.
- Cross-post entries store the resolved destination in `entry.original_entry`; navigation should target the same canonical identity that highlight/keyboard lookup/ref tracking uses.

Implement a single canonical identity helper that uses `entry.original_entry || entry`, and derive a normalized route identity (including `params.author`, with any leading `@` stripped as done elsewhere, e.g. blog-post-page.tsx). Use the same canonical author+permlink pair for: computing active/highlight state, computing the keyboard navigation “current index”, maintaining refs, and performing navigation so all these behaviors stay consistent.

## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[37-40]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[63-68]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[79-96]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[127-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. j/k listener active while rail hidden on mobile 🐞 Bug ☼ Reliability ⭐ New
Description
ReaderRail is always mounted inside ReaderShell's <aside>, which is only CSS-hidden ('hidden
lg:block') on non-feed routes at mobile widths; ReaderRail's window keydown listener for 'j'/'k'
stays registered regardless, so pressing those keys on a mobile post/search/publish page silently
navigates away even though the rail providing that context is not visible.
Code

apps/self-hosted/src/themes/reader/reader-shell.tsx[R99-106]

+        <aside
+          className={clsx(
+            'w-full lg:w-[var(--theme-sidebar-width)] lg:shrink-0 lg:border-r border-theme overflow-y-auto bg-theme-secondary',
+            !isFeedRoute && 'hidden lg:block',
+          )}
+        >
+          <ReaderRail filter={currentFilter} />
+        </aside>
Evidence
reader-shell.tsx hides the <aside> containing ReaderRail purely via Tailwind's 'hidden lg:block'
utility on non-feed routes, but does not unmount it. reader-rail.tsx's useEffect registers a
window-level keydown handler for 'j'/'k' with no dependency on visibility or route, and the handler
unconditionally calls navigate() once a next entry is found, so it will fire and redirect the page
even while the rail is invisible on small screens.

apps/self-hosted/src/themes/reader/reader-shell.tsx[99-106]
apps/self-hosted/src/themes/reader/reader-rail.tsx[70-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Reader rail's keyboard navigation (j/k) listener stays active on `window` even when the rail's `<aside>` is CSS-hidden on mobile for non-feed routes, causing unexpected navigation when a user presses j/k on a mobile post/search/publish page.

## Issue Context
`ReaderShell` always renders `<ReaderRail>` inside an `<aside>` that is hidden via `hidden lg:block` (not unmounted) whenever the route isn't the feed route and the viewport is below the `lg` breakpoint. `ReaderRail`'s `useEffect` attaches a `keydown` listener unconditionally.

## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-shell.tsx[98-106]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[70-102]

Either pass a visibility flag down (e.g. based on `isFeedRoute` or a media query) that gates whether the keydown listener is attached, or avoid rendering `ReaderRail` at all when it is not visible on small screens, mounting it only when the aside is actually shown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. useArchiveFeed uses any 📘 Rule violation ⚙ Maintainability
Description
The new useArchiveFeed hook introduces an explicit any type in its selectPosts callback, which
violates the project rule forbidding any in new/changed TypeScript code. This weakens type-safety
and can mask incorrect query result shapes.
Code

apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[R39-42]

+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const selectPosts = useCallback(
+    (data: { pages: any[][] }) => data.pages.flat(),
+    []
Relevance

●●● Strong

Team recently accepted removing newly introduced any/no-explicit-any escapes; likely will require
proper typing here.

PR-#1443

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new hook
explicitly types pages as any[][] (and suppresses the lint rule), which is a direct violation.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useArchiveFeed` introduces an explicit `any` type in the `selectPosts` callback (`(data: { pages: any[][] }) => ...`). The compliance rule disallows `any` in new/modified TypeScript.
## Issue Context
This hook is shared by both the default blog list and the Reader rail; keeping its query result types accurate is important to prevent silent runtime shape drift.
## Fix Focus Areas
- apps/self-hosted/src/features/blog/hooks/use-archive-feed.ts[39-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (2)
6. Selected filter is discarded 🐞 Bug ≡ Correctness
Description
ReaderRail post links omit the active filter, so opening a post from /blog?filter=hot makes
ReaderShell fall back to the first configured feed and replaces the persistent rail. The selected
post may consequently disappear from the rail.
Code

apps/self-hosted/src/themes/reader/reader-rail.tsx[139]

+            search={{ raw: undefined }}
Relevance

●●● Strong

They’ve accepted fixes preserving/setting route search state; likely will keep filter when
navigating from rail.

PR-#690
PR-#1441

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rail passes only raw when navigating, while ReaderShell obtains its feed from
usePostsFilterState; that hook defaults to the first configured filter whenever the current route
search has no filter. Both post route validators currently retain only raw.

apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
apps/self-hosted/src/themes/reader/reader-rail.tsx[131-140]
apps/self-hosted/src/themes/reader/reader-shell.tsx[44-45]
apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts[16-30]
apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reader post navigation drops the active archive filter, causing the rail to switch back to the configured default feed.
## Issue Context
Preserve and validate the Reader filter on post routes and links, or maintain Reader filter state independently of route search. Apply the same behavior to click and keyboard navigation.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-rail.tsx[93-97]
- apps/self-hosted/src/themes/reader/reader-rail.tsx[134-140]
- apps/self-hosted/src/routes/$author.$permlink.tsx[6-9]
- apps/self-hosted/src/routes/$category.$author.$permlink.tsx[6-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Publish drops Reader shell 🐞 Bug ≡ Correctness
Description
ReaderShell's CreatePostButton links to /publish, but that route renders a standalone layout
instead of resolving the theme Shell. Publishing therefore removes the Reader masthead and split
rail and renders the legacy sidebar layout despite the new shell explicitly treating publish as a
content-pane route.
Code

apps/self-hosted/src/themes/reader/reader-shell.tsx[R120-122]

+      {/* The floating composer entry point the default navigation mounts; a
+          theme shell must never cost owners and community members the way in. */}
+      <CreatePostButton />
Relevance

●● Moderate

Publish-in-shell behavior is architectural/theme-routing; no close precedent, though they care about
publish entry points.

PR-#1104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added ReaderShell exposes CreatePostButton, whose internal target is /publish. The publish
route directly returns its own min-h-screen grid and BlogSidebar, whereas BlogLayout is the
component that resolves and mounts the active theme Shell.

apps/self-hosted/src/themes/reader/reader-shell.tsx[14-22]
apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[52-56]
apps/self-hosted/src/routes/publish.tsx[37-61]
apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The publish entry point exits the Reader shell because the publish route bypasses BlogLayout/theme component resolution.
## Issue Context
Keep the existing publish authorization gate, but route authorized publish content through the Reader-aware shell or add an equivalent theme-aware route wrapper.
## Fix Focus Areas
- apps/self-hosted/src/themes/reader/reader-shell.tsx[120-122]
- apps/self-hosted/src/routes/publish.tsx[37-61]
- apps/self-hosted/src/features/blog/layout/blog-layout.tsx[10-12]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

8. Test file wrong location 📜 Skill insight ⌂ Architecture
Description
This PR modifies a test file that is co-located under src/features/... instead of the required
src/specs/... mapping, and it uses a .test.ts filename instead of the required
.spec.ts/.spec.tsx pattern. This breaks the project’s mandated test organization conventions.
Code

apps/self-hosted/src/features/shared/failure-states.test.ts[48]

+  'src/themes/reader/reader-rail.tsx:noPosts': 1,
Relevance

● Weak

Repo repeatedly rejected moving/renaming co-located *.test.ts to src/specs/*.spec.ts despite
policy claims.

PR-#1442
PR-#1443
PR-#1437

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668281 requires feature tests to live under the corresponding src/specs/...
subtree (not under src/features/...), and PR Compliance ID 2668286 requires .spec.ts/.spec.tsx
naming. The modified file is apps/self-hosted/src/features/shared/failure-states.test.ts and the
change occurs within it, confirming the test remains co-located and named .test.ts.

apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]
Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A modified test file is not in the required `src/specs/...` directory and does not follow the `.spec.*` naming convention.
## Issue Context
The checklist requires tests to be placed under `src/specs/...` (not co-located with source) and named using `.spec.ts` / `.spec.tsx`. This PR adds a new guarded-claim entry in `apps/self-hosted/src/features/shared/failure-states.test.ts`, keeping the file in a non-compliant location and name.
## Fix Focus Areas
- apps/self-hosted/src/features/shared/failure-states.test.ts[46-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
Review mode: 🧠 Deep: This adds substantial interactive layout logic across multiple components, shared data-fetching behavior, routing/keyboard navigation, responsive rendering, and registry/catalog integration, creating many independent defect opportunities.

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/self-hosted/src/themes/reader/reader-rail.tsx Outdated
Comment thread apps/self-hosted/src/themes/reader/reader-rail.tsx Outdated
Comment thread apps/self-hosted/src/themes/reader/reader-shell.tsx
…ing routes

The extracted hook's select callback types its pages as Entry[][] instead
of carrying the any over from its previous home. The rail's j/k listener
now only acts on the feed and open posts: the rail stays mounted but
CSS-hidden on search, publish and edit, and a stray keypress there must
never navigate away from a composer. Post routes stay active even where
the rail is hidden on small screens, since next and previous post is
exactly what those keys mean while reading.
@feruzm
feruzm merged commit 5dde702 into develop Aug 12, 2026
11 checks passed
@feruzm
feruzm deleted the feature/self-hosted-reader-theme branch August 12, 2026 12:44
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.

Self-hosted: Reader theme with a split archive view

1 participant