Skip to content

Self-hosted: Journal, the first layout-level theme - #1446

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

Self-hosted: Journal, the first layout-level theme#1446
feruzm merged 3 commits into
developfrom
feature/self-hosted-journal-theme

Conversation

@feruzm

@feruzm feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes #1420.

The first layout-level design on the theme manifest platform (#1442): Journal, ink on paper. One measure-width column, an author block as the masthead (avatar, title, description, a quiet filter row with search and the user menu), entries as date, large serif title and excerpt separated by hairline rules. No cards, no sidebar, no counters on the archive; stats and actions live on the post page. Warm paper light mode, warm charcoal dark mode; both accent inks are exactly what the correction module chooses for the same fills, so static CSS and the runtime preview agree, and the token-contract guard suite enforces it.

Platform pieces this exercises for the first time:

  • Journal's manifest overrides the Shell and PostCard seams; Navigation, Sidebar and ArchiveList fall back to the shared defaults. The registry tests evolve accordingly: the five original templates stay pinned as no-op manifests, Journal is asserted structural with defaults for the seams it does not own.
  • The manifest gains unsupportedOptions, the explicit declaration the contract demanded for config toggles a theme's components do not consume. The editor hides the sidebar section and the list-type select under Journal through the first real visibleWhen predicates (Self-hosted: per-theme option visibility in the Configuration Editor #1443), reading the EDITED document so the panel reacts to an unsaved template switch. Stored values are untouched and apply again the moment the owner switches back.
  • Search results resolve their entry card through the registry, so search wears the active theme's look instead of the default card.
  • The roster, signup catalog card, editor label and CSS registry all extend through their existing forcing functions (satisfies maps, the roster-to-CSS guard suite, the token sweep with its template counts bumped to six).

Verified: SPA 906 tests, hosting API 419 (the catalog test now covers six cards), typecheck, production build with the node-globals guard, lint clean. Visual pass: the theme activates only when chosen, so it ships dormant; preview it live on any instance through the Configuration Editor (pick Journal, Preview) and iterate from there.

Summary by CodeRabbit

  • New Features

    • Added the Journal theme with a centered, single-column layout, serif typography, warm light/dark styling, and no sidebar.
    • Added Journal-specific post cards displaying dates, titles, summaries, and author links.
    • Added Journal configuration and translated theme selection labels.
    • Search results now match the active theme’s post-card styling.
  • Improvements

    • Theme settings now hide unsupported sidebar and list-layout options automatically.

Ink on paper: one measure-width column, an author block as the
masthead, entries as date, large serif title and excerpt separated by
hairline rules. No cards, no sidebar, no counters on the archive; warm
paper light mode and warm charcoal dark mode, with accent inks matching
exactly what the correction module chooses for the same fills, so
static CSS and runtime preview agree (guard-tested).

The structural half exercises the manifest platform end to end:
journal overrides the Shell and PostCard seams and everything else
falls back to the shared defaults. The manifest gains
unsupportedOptions, the explicit declaration the contract demanded for
toggles a theme's components do not consume: the editor hides the
sidebar section and the list-type select under Journal through the
first real visibleWhen predicates, reading the EDITED document so the
panel reacts to an unsaved template switch, while stored values stay
untouched and apply again on switching back. Search results now
resolve their entry card through the registry, so search wears the
active theme's look.

The roster, catalog card, editor label and CSS registry all extend
through their existing forcing functions (satisfies maps, the
roster-to-CSS guard suite, the token-contract sweep with its counts
bumped for a sixth template).

Closes #1420
@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add Journal layout-level theme with manifest-gated layout options

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

Grey Divider

AI Description

• Add Journal, the first structural theme overriding Shell and PostCard seams.
• Hide unsupported layout toggles via manifest unsupportedOptions and visibleWhen.
• Route search results through the theme registry so cards match the active theme.
Diagram

graph TD
A["Configuration Editor"] --> B["buildConfigFields visibleWhen"] --> C["Theme registry"] --> D["Journal manifest (components + unsupportedOptions)"]
E["InstanceConfigManager styleTemplate"] --> F["useThemeComponents"] --> G["Blog UI (feeds + search)"]
D --> G --> H["Journal CSS tokens"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Hardcode template-based UI hiding in the editor
  • ➕ Fewer moving pieces; no new manifest surface area
  • ➖ Violates the manifest contract goal (capabilities should live with the theme)
  • ➖ Becomes brittle as more structural themes/options are added
2. Infer support from component presence (e.g., if Shell overrides then hide sidebar)
  • ➕ No explicit unsupportedOptions list to maintain
  • ➖ Couples unrelated concepts (component override != option support)
  • ➖ Breaks for themes that override Shell but still support a sidebar, or vice versa
3. Make options per-seam capability metadata (e.g., Sidebar component declares supported keys)
  • ➕ More granular; scales to many options and seams
  • ➖ More complexity and boilerplate for the first structural theme
  • ➖ Harder to reason about than a single manifest declaration for now

Recommendation: Keep the PR’s explicit unsupportedOptions manifest declaration. It cleanly encodes capability ownership with the theme, avoids editor hardcoding, and stays flexible as future structural themes mix-and-match seams and supported layout toggles. The current implementation is minimal yet extensible (new options only require expanding ThemeOptionKey and updating manifests/tests).

Files changed (14) +451 / -11

Enhancement (10) +356 / -3
style-template-display.tsAdd Journal to template catalog display metadata +11/-0

Add Journal to template catalog display metadata

• Introduces the Journal catalog entry (name, tagline, palette, heading style) so the hosting API can present it consistently in pickers/catalogs.

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

style-templates.tsRegister 'journal' in the style template roster +1/-0

Register 'journal' in the style template roster

• Adds 'journal' to the STYLE_TEMPLATES list, extending the StyleTemplate union and making the template available across the platform.

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

i18n-strings.tsAdd i18n key for the Journal template label +3/-0

Add i18n key for the Journal template label

• Extends TranslationKey and English strings with a Journal option label for the configuration panel template selector.

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

search-results.tsxRender search results via theme-resolved PostCard +5/-2

Render search results via theme-resolved PostCard

• Replaces the direct BlogPostItem usage with the PostCard seam from useThemeComponents so search results match the active theme’s entry presentation.

apps/self-hosted/src/features/blog/components/search-results.tsx

index.cssImport Journal theme CSS into the theme registry stylesheet +1/-0

Import Journal theme CSS into the theme registry stylesheet

• Adds the journal.css import so Journal tokens are available when the template is active.

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

journal.cssIntroduce Journal theme tokens (light/dark) and layout typography +114/-0

Introduce Journal theme tokens (light/dark) and layout typography

• Adds a full --theme-* token implementation for Journal, including warm paper/charcoal palettes, serif-forward typography, no-elevation effects, and single-column layout tokens while still satisfying the global token contract.

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

journal-post-card.tsxAdd JournalPostCard: minimal entry rendering without card chrome +58/-0

Add JournalPostCard: minimal entry rendering without card chrome

• Implements Journal’s PostCard seam: date + title + excerpt with a hairline rule, omitting images and counters while preserving canonical router link structure.

apps/self-hosted/src/themes/journal/journal-post-card.tsx

journal-shell.tsxAdd JournalShell: single-column masthead layout without sidebar +127/-0

Add JournalShell: single-column masthead layout without sidebar

• Implements Journal’s Shell seam with a centered column, author/community masthead (avatar/title/description), quiet filter navigation, and integrated search + user menu, explicitly rendering no sidebar.

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

manifest.tsExtend theme manifest contract with unsupportedOptions +11/-0

Extend theme manifest contract with unsupportedOptions

• Adds ThemeOptionKey and an optional unsupportedOptions declaration so themes can explicitly state which config toggles they do not consume, enabling the editor to avoid silently inert controls.

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

registry.tsRegister Journal manifest and expose isThemeOptionSupported() +25/-1

Register Journal manifest and expose isThemeOptionSupported()

• Adds Journal to the manifest map with Shell/PostCard overrides and unsupportedOptions, and introduces isThemeOptionSupported() for editor predicates to query capabilities via manifest rather than hardcoded template ids.

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

Tests (3) +71 / -8
config-fields.test.tsTest theme-gated visibility for sidebar and list-type options +29/-0

Test theme-gated visibility for sidebar and list-type options

• Adds tests asserting that Journal hides sidebar/list-type controls, that CSS-only templates keep them visible, and that predicates follow the unsaved edited draft.

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

theme-appearance-tokens.test.tsUpdate token contract tests for sixth theme +4/-4

Update token contract tests for sixth theme

• Bumps expected counts for accent palette blocks and ensures card-treatment coverage includes the new Journal theme.

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

registry.test.tsEvolve registry tests for first structural theme + option support +38/-4

Evolve registry tests for first structural theme + option support

• Keeps the original five templates asserted as CSS-only manifests, adds Journal structural expectations (Shell/PostCard overrides + unsupportedOptions), and tests isThemeOptionSupported behavior and component fallback defaults.

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

Other (1) +24 / -0
config-fields.tsHide layout fields when active theme declares them unsupported +24/-0

Hide layout fields when active theme declares them unsupported

• Adds an editedStyleTemplate helper and wires visibleWhen predicates for listType and sidebar to isThemeOptionSupported, ensuring visibility reacts immediately to unsaved template changes. Also adds the Journal template label mapping.

apps/self-hosted/src/features/floating-menu/config-fields.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: 186c8cb6b2

ℹ️ 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".

Comment on lines +114 to +117
<span className="ml-auto flex items-center gap-3">
<SearchInput />
<UserMenu />
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the create-post entry point in Journal

When Journal is active, this action area renders only search and the user menu, while the shell replaces BlogNavigation, the only component found in the SPA that mounts CreatePostButton. Consequently, authenticated community members and blog owners lose the visible route to the internal publisher or configured external create-post URL and can publish only by already knowing the destination; render CreatePostButton from the Journal shell as well.

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.

Fixed in 6d44b32: JournalShell now mounts CreatePostButton, and the commit records this as the standing contract for any theme shell. Good catch, this was the one real functional loss in the redesign.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 41 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: 2a5d87d8-e130-4e53-a883-837b46ecdf1c

📥 Commits

Reviewing files that changed from the base of the PR and between 186c8cb and b6cf8f8.

📒 Files selected for processing (5)
  • apps/self-hosted/src/features/blog/hooks/use-posts-filter-state.ts
  • apps/self-hosted/src/features/blog/layout/blog-navigation.tsx
  • apps/self-hosted/src/features/floating-menu/config-fields.test.ts
  • apps/self-hosted/src/styles/themes/journal.css
  • apps/self-hosted/src/themes/journal/journal-shell.tsx
📝 Walkthrough

Walkthrough

The PR adds the Journal style template with dedicated metadata, styling, layout, post-card rendering, theme registration, search integration, and configuration controls that hide unsupported sidebar and list options.

Changes

Journal theme

Layer / File(s) Summary
Template and option contracts
apps/self-hosted/hosting/api/src/style-templates.ts, apps/self-hosted/hosting/api/src/style-template-display.ts, apps/self-hosted/src/core/i18n-strings.ts, apps/self-hosted/src/themes/manifest.ts
The template roster, display metadata, translation catalog, and theme manifest now support Journal and its unsupported layout options.
Theme registry and Journal components
apps/self-hosted/src/themes/registry.ts, apps/self-hosted/src/themes/journal/*, apps/self-hosted/src/themes/registry.test.ts
The registry provides Journal-specific shell and post-card components. The components render the single-column layout and journal entries. Registry tests cover resolution and option support.
Journal styling and themed search results
apps/self-hosted/src/styles/themes/*, apps/self-hosted/src/features/blog/components/search-results.tsx, apps/self-hosted/src/styles/theme-appearance-tokens.test.ts
Light and dark Journal tokens are imported and tested. Search results now render the active theme’s PostCard.
Draft-aware configuration visibility
apps/self-hosted/src/features/floating-menu/config-fields.ts, apps/self-hosted/src/features/floating-menu/config-fields.test.ts
Sidebar and list-type fields are hidden when the edited theme does not support them. Tests cover saved, unset, supported, and unsaved template values.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant JournalShell
  participant SearchResults
  participant ThemeRegistry
  participant JournalPostCard
  Browser->>JournalShell: Render Journal page
  JournalShell->>SearchResults: Render page children
  SearchResults->>ThemeRegistry: Resolve PostCard
  ThemeRegistry-->>SearchResults: Return JournalPostCard
  SearchResults->>JournalPostCard: Render entry data
Loading

Possibly related PRs

Poem

A rabbit found a journal page,
With serif words on paper beige.
One column, quiet cards, warm light,
Dark accents glow through reading night.
The sidebar hops away—goodbye! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies Journal as a new layout-level theme for self-hosted instances.
Linked Issues check ✅ Passed The changes implement Journal with a single-column serif layout, no cards or sidebar, and theme-specific shell and post-card components.
Out of Scope Changes check ✅ Passed The changes support Journal registration, rendering, editor behavior, styling, and tests without unrelated code changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/self-hosted-journal-theme

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/self-hosted/src/styles/themes/journal.css`:
- Around line 15-17: Update the --theme-font-ui declaration to use lowercase
font-family keyword values for BlinkMacSystemFont and Arial, while preserving
the existing font fallback order and other values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 67978e6b-3fac-421c-90d6-3bececc43685

📥 Commits

Reviewing files that changed from the base of the PR and between 6928d24 and 186c8cb.

📒 Files selected for processing (14)
  • 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/search-results.tsx
  • apps/self-hosted/src/features/floating-menu/config-fields.test.ts
  • apps/self-hosted/src/features/floating-menu/config-fields.ts
  • apps/self-hosted/src/styles/theme-appearance-tokens.test.ts
  • apps/self-hosted/src/styles/themes/index.css
  • apps/self-hosted/src/styles/themes/journal.css
  • apps/self-hosted/src/themes/journal/journal-post-card.tsx
  • apps/self-hosted/src/themes/journal/journal-shell.tsx
  • apps/self-hosted/src/themes/manifest.ts
  • apps/self-hosted/src/themes/registry.test.ts
  • apps/self-hosted/src/themes/registry.ts

Comment thread apps/self-hosted/src/styles/themes/journal.css
@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Grey Divider


Action required

1. Journal Shell drops CreatePostButton ✓ Resolved 🐞 Bug ≡ Correctness
Description
JournalShell fully replaces the Shell seam for the Journal theme but its masthead action row renders
only SearchInput and UserMenu, bypassing BlogNavigation and thereby omitting CreatePostButton (and
RssFeedLink). As a result, blog owners and other permitted users on the Journal theme lose the
primary, discoverable in-app entry point to start creating a new post from the blog/archive page.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R114-118]

+            <span className="ml-auto flex items-center gap-3">
+              <SearchInput />
+              <UserMenu />
+            </span>
+          </div>
Relevance

●● Moderate

Omitting CreatePost/RSS may be intentional for Journal; no close precedent confirming team will add
them back.

PR-#1104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In apps/self-hosted/src/features/blog/layout/blog-navigation.tsx, the default BlogNavigation
component (used by the standard shell for other themes) renders an action row that includes
SearchInput, RssFeedLink, CreatePostButton, and UserMenu, and it is also described as the
repository’s only call site of CreatePostButton. The Journal theme’s manifest/registry installs
JournalShell as the active shell override, and in
apps/self-hosted/src/themes/journal/journal-shell.tsx the masthead action row renders only
SearchInput and UserMenu with no CreatePostButton or RssFeedLink elsewhere in the file,
demonstrating that switching to Journal removes these actions from the UI.

apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
apps/self-hosted/src/themes/journal/journal-shell.tsx[113-118]
apps/self-hosted/src/themes/registry.ts[28-32]
apps/self-hosted/src/features/blog/layout/default-shell.tsx[12-27]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[35-55]

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 `JournalShell` component replaces the default page shell for the Journal theme, but its masthead action row only renders `<SearchInput />` and `<UserMenu />`, omitting `<CreatePostButton />` (and `<RssFeedLink />`) that are present in the default `BlogNavigation`. Because `BlogNavigation` is the only existing caller of `CreatePostButton` and Journal bypasses it entirely, users with permission (blog owners and permitted community users) lose the only visible/discoverable in-app entry point to publishing from the blog/archive page.

## Issue Context
- `JournalShell` (`apps/self-hosted/src/themes/journal/journal-shell.tsx`) is registered as the `Shell` override for the Journal theme via the theme manifest/registry (`apps/self-hosted/src/themes/registry.ts`), so it fully replaces `DefaultShell` + `BlogNavigation`.
- `BlogNavigation` (`apps/self-hosted/src/features/blog/layout/blog-navigation.tsx`) renders the complete action row (including `CreatePostButton` and `RssFeedLink`) and is the sole call site of `CreatePostButton` in the repository; therefore, any theme that does not render `BlogNavigation` must explicitly preserve those actions.

## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[1-20]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[113-118]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[114-117]
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
- apps/self-hosted/src/themes/registry.ts[1-20]

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



Remediation recommended

2. as any in test ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test code introduces an explicit any cast via (fields.configuration as any), which
violates the ban on any in new/modified TypeScript. This weakens type safety and can hide real
schema/type regressions in config-field construction.
Code

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[869]

+  const layout = (fields.configuration as any).fields.instanceConfiguration.fields.layout.fields;
Relevance

●●● Strong

Repo has accepted removing newly introduced any in TS tests; same file precedent.

PR-#1443

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule forbids introducing any in modified TypeScript. The added test line uses
(fields.configuration as any) to bypass typing, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-870]

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 new test uses an explicit `any` cast: `(fields.configuration as any)`, which violates the rule disallowing `any` in new/modified TypeScript code.

## Issue Context
The test is accessing nested fields under `buildConfigFields(...)`. This should be done with a proper type for `fields.configuration` (or a narrower helper type) rather than opting out of type checking.

## Fix Focus Areas
- apps/self-hosted/src/features/floating-menu/config-fields.test.ts[868-875]

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


3. Muted text fails contrast ✓ Resolved 🐞 Bug ≡ Correctness
Description
Journal's light palette composites its muted text token to approximately #83807a over #faf8f4,
yielding only about 3.75:1 contrast. The new components apply this token to 14px dates and filter
labels, below the 4.5:1 requirement for normal text.
Code

apps/self-hosted/src/styles/themes/journal.css[33]

+  --theme-text-muted: rgba(34, 29, 23, 0.55);
Relevance

●● Moderate

Contrast issues are plausible but no clear accepted/rejected precedent for muted text token
contrast.

PR-#1355

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The stylesheet defines the light background and translucent muted foreground together.
JournalPostCard uses the token on a text-sm date, and JournalShell uses it on text-sm inactive
filter links, so the sub-4.5:1 value affects normal-sized visible text.

apps/self-hosted/src/styles/themes/journal.css[25-33]
apps/self-hosted/src/themes/journal/journal-post-card.tsx[36-40]
apps/self-hosted/src/themes/journal/journal-shell.tsx[103-107]

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 Journal light-mode muted text color does not provide sufficient contrast for normal-sized text.

## Issue Context
`rgba(34, 29, 23, 0.55)` over `#faf8f4` produces roughly 3.75:1 contrast, while Journal uses it for `text-sm` informational and navigational text. Choose a darker/less-transparent token that reaches at least 4.5:1 and add coverage for text-token contrast.

## Fix Focus Areas
- apps/self-hosted/src/styles/themes/journal.css[25-33]
- apps/self-hosted/src/themes/journal/journal-post-card.tsx[36-40]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[103-107]

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


4. Expanded masthead overflows ✓ Resolved 🐞 Bug ≡ Correctness
Description
At a 320px viewport the Journal container leaves 280px after padding, but the authenticated
expanded-search controls require about 284px before the row's 16px gap or any filter navigation.
Opening search therefore forces the masthead beyond the viewport or clips controls on narrow phones.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R96-97]

+          <div className="mt-8 flex items-center justify-center gap-4 border-y border-theme py-2">
+            <nav className="flex items-center gap-5 overflow-x-auto">
Relevance

●● Moderate

Responsive overflow concern is plausible but needs UI judgment; no close historical precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Journal's 1.25rem side padding leaves 280px at a 320px viewport. SearchInput's expanded form is
approximately 196px (160px input, 8px gap, 28px close button), and the authenticated UserMenu button
is approximately 76px at mobile widths; with the controls' 12px gap they already total about 284px,
before Journal's 16px nav-to-controls gap.

apps/self-hosted/src/themes/journal/journal-shell.tsx[69-72]
apps/self-hosted/src/themes/journal/journal-shell.tsx[96-117]
apps/self-hosted/src/features/blog/components/search-input.tsx[48-75]
apps/self-hosted/src/features/auth/components/user-menu.tsx[63-83]

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 Journal masthead cannot fit expanded search, the authenticated user control, and filter navigation in one row at 320px widths.

## Issue Context
The theme applies 1.25rem horizontal padding. The expanded search includes a fixed `w-40` input, close button, and gaps; the authenticated user button adds an avatar and chevron even while its username is hidden.

## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[96-117]
- apps/self-hosted/src/features/blog/components/search-input.tsx[48-75]

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


View review recommended (1)
5. Journal shell duplicates filter/label logic ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
JournalShell reimplements availableFilters/currentFilter/filterLabel logic nearly verbatim from
BlogNavigation instead of extracting or reusing a shared hook, so any future bugfix or behavior
change to filter resolution (e.g. the post-filters hardening in PR #1156) must be applied twice and
can silently drift between themes.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R44-67]

+  const currentFilter = useMemo(() => {
+    const defaultFilter = availableFilters[0] || 'posts';
+    if (typeof location.search === 'string') {
+      return new URLSearchParams(location.search).get('filter') || defaultFilter;
+    }
+    if (
+      location.search &&
+      typeof location.search === 'object' &&
+      'filter' in location.search
+    ) {
+      return (location.search.filter as string) || defaultFilter;
+    }
+    return defaultFilter;
+  }, [location.search, availableFilters]);
+
+  // Mirrors blog-navigation's label resolution: an i18n key when one exists,
+  // a capitalized filter name otherwise.
+  const filterLabel = (filter: string): string => {
+    const key = `blog.navigation.${filter}`;
+    const translated = t(key as Parameters<typeof t>[0]);
+    return translated === key
+      ? filter.charAt(0).toUpperCase() + filter.slice(1)
+      : translated;
+  };
Relevance

●● Moderate

Refactor/extraction request is subjective; no close historical acceptance/rejection found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The currentFilter useMemo and filterLabel function in journal-shell.tsx duplicate the identical
currentFilter/getFilterLabel logic already implemented in blog-navigation.tsx, meaning the same
URLSearchParams parsing and i18n-key-fallback logic exists in two independently maintained files.

apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[24-76]
apps/self-hosted/src/themes/journal/journal-shell.tsx[44-67]

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

## Issue description
`JournalShell` duplicates the `currentFilter` resolution and `filterLabel`/`getFilterLabel` logic that already exists in `BlogNavigation`, instead of importing a shared implementation. This creates two independent copies of the same URL-parsing and i18n-fallback logic that must be kept in sync manually.

## Issue Context
Both components read `getConfiguredPostsFilters()` and then derive the active filter from `location.search`, with an identical fallback path for community/blog labels. Any future defect fix (e.g. malformed search param handling) applied to one copy will not automatically apply to the other.

## Fix Focus Areas
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[24-76]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[44-67]

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



Informational

6. Journal card omits image, no listType support ✗ Dismissed 🐞 Bug ⚙ Maintainability
Description
JournalPostCard never renders a post's image even though the shared PostCard contract and
listType='grid' option imply images are part of the archive experience elsewhere in the app;
combined with declaring 'listType' unsupported, an owner cannot get any thumbnail on the Journal
archive even though other config (image proxy, post images) remains fully populated and functional
on the post page. This is by design per the PR description, so treat as informational rather than a
defect, but flag as a design/maintainability note since the manifest silently drops config the owner
may expect to still influence rendering (e.g. --theme-post-card-image-height/radius tokens declared
in journal.css are unused by JournalPostCard).
Code

apps/self-hosted/src/styles/themes/journal.css[R82-83]

+  --theme-post-card-image-height: 200px;
+  --theme-post-card-image-radius: 2px;
Relevance

●● Moderate

Design note; no clear precedent that unused theme tokens/images must be implemented for every theme.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
journal.css declares --theme-post-card-image-height and --theme-post-card-image-radius tokens (lines
82-83), part of the full theme-* contract, but JournalPostCard (journal-post-card.tsx) never renders
an <img> element or reads these tokens, leaving them dead in this theme.

apps/self-hosted/src/styles/themes/journal.css[82-83]
apps/self-hosted/src/themes/journal/journal-post-card.tsx[35-57]


7. Misplaced .test.ts in features 📜 Skill insight ⌂ Architecture
Description
New tests were added to a co-located *.test.ts file under src/features/, but tests must live
under the corresponding src/specs/ path and follow the .spec.ts/.spec.tsx naming convention.
Keeping tests in the wrong location/name breaks the required test organization standards and makes
discovery/enforcement inconsistent.
Code

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[R867-870]

+describe('theme-gated layout options', () => {
+  const fields = buildConfigFields((key) => key);
+  const layout = (fields.configuration as any).fields.instanceConfiguration.fields.layout.fields;
+
Relevance

● Weak

Exact precedent: test rename/move to src/specs + .spec.* was rejected in same file.

PR-#1443
PR-#1440

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a new describe block to
apps/self-hosted/src/features/floating-menu/config-fields.test.ts, which is both co-located under
src/features/ and uses a .test.ts filename, conflicting with the required src/specs/...
placement and .spec.* naming conventions.

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-894]
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
Tests were added to `apps/self-hosted/src/features/floating-menu/config-fields.test.ts`, but compliance requires tests to be placed under the mapped `src/specs/...` directory and to use the `.spec.ts` / `.spec.tsx` naming pattern.

## Issue Context
This PR adds a new `describe('theme-gated layout options', ...)` block. Because it is being introduced/modified in this PR, it must comply with the test placement and naming rules.

## Fix Focus Areas
- apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-894]

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


Grey Divider

Context
✅ Compliance rules (platform): 75 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
✅ Web pages:
  +12 more
Review mode: 🧠 Deep: This adds a new layout theme across registry, rendering components, search integration, editor visibility/config contracts, CSS tokens, and hosting catalog, creating multiple independent paths where a redundant review pass can catch subtle integration defects.

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/floating-menu/config-fields.test.ts Outdated
Comment thread apps/self-hosted/src/themes/journal/journal-shell.tsx Outdated
Comment thread apps/self-hosted/src/styles/themes/journal.css Outdated
Comment thread apps/self-hosted/src/themes/journal/journal-shell.tsx
Comment thread apps/self-hosted/src/styles/themes/journal.css
Comment thread apps/self-hosted/src/themes/journal/journal-shell.tsx Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Publishing control disappears 🐞 Bug ≡ Correctness ⭐ New
Description
JournalShell replaces the default navigation but mounts only SearchInput and UserMenu, so
CreatePostButton disappears whenever Journal is active. Blog owners and authenticated community
users lose the visible entry point to the composer even though their publishing permission and route
still exist.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R115-116]

+              <SearchInput />
+              <UserMenu />
Evidence
The Journal manifest replaces the entire shared shell, while the new action row omits the only
component that applies publishing eligibility and links eligible users to the composer. The default
navigation previously mounted that component for every theme.

apps/self-hosted/src/themes/registry.ts[28-32]
apps/self-hosted/src/themes/journal/journal-shell.tsx[114-117]
apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[35-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
Journal's replacement shell omits `CreatePostButton`, removing the visible publishing entry point for eligible users.

## Issue Context
The default navigation mounts this permission-aware control; it already hides itself for users who cannot publish and selects the correct internal or external composer target.

## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[114-117]
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
- apps/self-hosted/src/features/auth/components/create-post-button.tsx[16-69]

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


2. Journal Shell drops CreatePostButton 🐞 Bug ≡ Correctness
Description
JournalShell fully replaces the Shell seam for the Journal theme but its masthead action row renders
only SearchInput and UserMenu, bypassing BlogNavigation and thereby omitting CreatePostButton (and
RssFeedLink). As a result, blog owners and other permitted users on the Journal theme lose the
primary, discoverable in-app entry point to start creating a new post from the blog/archive page.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R114-118]

+            <span className="ml-auto flex items-center gap-3">
+              <SearchInput />
+              <UserMenu />
+            </span>
+          </div>
Relevance

●● Moderate

Omitting CreatePost/RSS may be intentional for Journal; no close precedent confirming team will add
them back.

PR-#1104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In apps/self-hosted/src/features/blog/layout/blog-navigation.tsx, the default BlogNavigation
component (used by the standard shell for other themes) renders an action row that includes
SearchInput, RssFeedLink, CreatePostButton, and UserMenu, and it is also described as the
repository’s only call site of CreatePostButton. The Journal theme’s manifest/registry installs
JournalShell as the active shell override, and in
apps/self-hosted/src/themes/journal/journal-shell.tsx the masthead action row renders only
SearchInput and UserMenu with no CreatePostButton or RssFeedLink elsewhere in the file,
demonstrating that switching to Journal removes these actions from the UI.

apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
apps/self-hosted/src/themes/journal/journal-shell.tsx[113-118]
apps/self-hosted/src/themes/registry.ts[28-32]
apps/self-hosted/src/features/blog/layout/default-shell.tsx[12-27]
apps/self-hosted/src/features/auth/components/create-post-button.tsx[35-55]

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 `JournalShell` component replaces the default page shell for the Journal theme, but its masthead action row only renders `<SearchInput />` and `<UserMenu />`, omitting `<CreatePostButton />` (and `<RssFeedLink />`) that are present in the default `BlogNavigation`. Because `BlogNavigation` is the only existing caller of `CreatePostButton` and Journal bypasses it entirely, users with permission (blog owners and permitted community users) lose the only visible/discoverable in-app entry point to publishing from the blog/archive page.
## Issue Context
- `JournalShell` (`apps/self-hosted/src/themes/journal/journal-shell.tsx`) is registered as the `Shell` override for the Journal theme via the theme manifest/registry (`apps/self-hosted/src/themes/registry.ts`), so it fully replaces `DefaultShell` + `BlogNavigation`.
- `BlogNavigation` (`apps/self-hosted/src/features/blog/layout/blog-navigation.tsx`) renders the complete action row (including `CreatePostButton` and `RssFeedLink`) and is the sole call site of `CreatePostButton` in the repository; therefore, any theme that does not render `BlogNavigation` must explicitly preserve those actions.
## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[1-20]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[113-118]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[114-117]
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[100-105]
- apps/self-hosted/src/themes/registry.ts[1-20]

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



Remediation recommended

3. Community description mismatched 🐞 Bug ≡ Correctness ⭐ New
Description
In community mode, JournalShell derives the title and avatar from community data but always renders
instanceConfiguration.meta.description. Communities whose instance description differs from
community.about therefore get a masthead combining two identities and omit their community
description.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R90-93]

+          {blogDescription && (
+            <p className="text-theme-muted italic max-w-md mx-auto">
+              {blogDescription}
+            </p>
Evidence
JournalShell fetches community data and uses its title and name for the masthead identity, but its
description remains unconditionally sourced from instance metadata. The existing community-aware
navigation proves community.about is the established description source.

apps/self-hosted/src/themes/journal/journal-shell.tsx[22-41]
apps/self-hosted/src/themes/journal/journal-shell.tsx[90-93]
apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[50-51]
apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[93-96]

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

## Issue description
Journal's community masthead uses the community title and avatar but displays the instance metadata description instead of the community's about text.

## Issue Context
The shared navigation already uses `community.about` in community mode. Preserve an appropriate fallback to the configured instance description when community data or its about field is absent.

## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[22-41]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[90-93]
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[50-51]
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[93-96]

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


4. Journal theme incomplete accent token contract 🐞 Bug ⚙ Maintainability ⭐ New
Description
The new journal.css declares --theme-accent, --theme-accent-hover, and --theme-accent-contrast for
both light and dark blocks, but omits --theme-accent-shade, which every other theme in variables.css
declares per-mode as part of the same accent token set. The manifest.ts contract explicitly states
every theme keeps 'the full --theme-* token contract', and other consumers of --theme-accent-shade
(e.g. apply-config-dom.ts writing it inline) could read an inconsistent or absent value under the
Journal theme when the accent is not explicitly configured by the owner.
Code

apps/self-hosted/src/styles/themes/journal.css[R35-38]

+  --theme-accent: #9c4a1e;
+  --theme-accent-hover: #7f3a15;
+  /* Ink on the accent fill: white on #9c4a1e is 5.4:1. */
+  --theme-accent-contrast: #ffffff;
Evidence
variables.css declares --theme-accent-shade alongside --theme-accent-contrast for both light and
dark modes (lines 57-60 and the dark equivalent), and apply-config-dom.ts writes
'--theme-accent-shade' inline (line 265) as part of the accent appearance contract that manifest.ts
documents every theme must keep ('The full --theme-* token contract'). journal.css's light block
(lines 26-51) and dark block (lines 87-114) declare --theme-accent, --theme-accent-hover and
--theme-accent-contrast but never --theme-accent-shade, so the theme is missing one token from the
documented per-theme contract; the existing guard test in theme-appearance-tokens.test.ts only
checks --theme-accent-shade presence on variables.css blocks, not per-theme files, so this gap is
not caught by CI.

apps/self-hosted/src/styles/themes/journal.css[26-51]
apps/self-hosted/src/styles/themes/variables.css[57-60]
apps/self-hosted/src/core/apply-config-dom.ts[265-265]
apps/self-hosted/src/themes/manifest.ts[13-20]

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

## Issue description
journal.css is missing the `--theme-accent-shade` custom property in both its light and dark mode blocks, while every other theme (declared in `variables.css`) declares this token alongside `--theme-accent`, `--theme-accent-hover`, and `--theme-accent-contrast`. The manifest contract in `manifest.ts` states every theme must keep 'the full --theme-* token contract'.

## Issue Context
`--theme-accent-shade` is consumed by `ACCENT_HOVER`'s `color-mix()` formula and is written inline by `apply-config-dom.ts` when an owner configures a custom accent. Its purpose is to give the hover state a direction to walk the accent fill towards, away from the ink placed on it (see `accentShadeFor` in `theme-appearance.ts`). Omitting it from Journal's static CSS means the theme's declared accent tokens are incomplete relative to the documented per-theme contract, and any code path expecting this token under the Journal theme when no accent override is configured has no theme-declared value to read.

## Fix Focus Areas
- apps/self-hosted/src/styles/themes/journal.css[26-51]
- apps/self-hosted/src/styles/themes/journal.css[87-114]

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


5. as any in test 📘 Rule violation ⚙ Maintainability
Description
The new test code introduces an explicit any cast via (fields.configuration as any), which
violates the ban on any in new/modified TypeScript. This weakens type safety and can hide real
schema/type regressions in config-field construction.
Code

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[869]

+  const layout = (fields.configuration as any).fields.instanceConfiguration.fields.layout.fields;
Relevance

●●● Strong

Repo has accepted removing newly introduced any in TS tests; same file precedent.

PR-#1443

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule forbids introducing any in modified TypeScript. The added test line uses
(fields.configuration as any) to bypass typing, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-870]

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 new test uses an explicit `any` cast: `(fields.configuration as any)`, which violates the rule disallowing `any` in new/modified TypeScript code.
## Issue Context
The test is accessing nested fields under `buildConfigFields(...)`. This should be done with a proper type for `fields.configuration` (or a narrower helper type) rather than opting out of type checking.
## Fix Focus Areas
- apps/self-hosted/src/features/floating-menu/config-fields.test.ts[868-875]

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


View review recommended (3)
6. Expanded masthead overflows 🐞 Bug ≡ Correctness
Description
At a 320px viewport the Journal container leaves 280px after padding, but the authenticated
expanded-search controls require about 284px before the row's 16px gap or any filter navigation.
Opening search therefore forces the masthead beyond the viewport or clips controls on narrow phones.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R96-97]

+          <div className="mt-8 flex items-center justify-center gap-4 border-y border-theme py-2">
+            <nav className="flex items-center gap-5 overflow-x-auto">
Relevance

●● Moderate

Responsive overflow concern is plausible but needs UI judgment; no close historical precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Journal's 1.25rem side padding leaves 280px at a 320px viewport. SearchInput's expanded form is
approximately 196px (160px input, 8px gap, 28px close button), and the authenticated UserMenu button
is approximately 76px at mobile widths; with the controls' 12px gap they already total about 284px,
before Journal's 16px nav-to-controls gap.

apps/self-hosted/src/themes/journal/journal-shell.tsx[69-72]
apps/self-hosted/src/themes/journal/journal-shell.tsx[96-117]
apps/self-hosted/src/features/blog/components/search-input.tsx[48-75]
apps/self-hosted/src/features/auth/components/user-menu.tsx[63-83]

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 Journal masthead cannot fit expanded search, the authenticated user control, and filter navigation in one row at 320px widths.
## Issue Context
The theme applies 1.25rem horizontal padding. The expanded search includes a fixed `w-40` input, close button, and gaps; the authenticated user button adds an avatar and chevron even while its username is hidden.
## Fix Focus Areas
- apps/self-hosted/src/themes/journal/journal-shell.tsx[96-117]
- apps/self-hosted/src/features/blog/components/search-input.tsx[48-75]

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


7. Muted text fails contrast 🐞 Bug ≡ Correctness
Description
Journal's light palette composites its muted text token to approximately #83807a over #faf8f4,
yielding only about 3.75:1 contrast. The new components apply this token to 14px dates and filter
labels, below the 4.5:1 requirement for normal text.
Code

apps/self-hosted/src/styles/themes/journal.css[33]

+  --theme-text-muted: rgba(34, 29, 23, 0.55);
Relevance

●● Moderate

Contrast issues are plausible but no clear accepted/rejected precedent for muted text token
contrast.

PR-#1355

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The stylesheet defines the light background and translucent muted foreground together.
JournalPostCard uses the token on a text-sm date, and JournalShell uses it on text-sm inactive
filter links, so the sub-4.5:1 value affects normal-sized visible text.

apps/self-hosted/src/styles/themes/journal.css[25-33]
apps/self-hosted/src/themes/journal/journal-post-card.tsx[36-40]
apps/self-hosted/src/themes/journal/journal-shell.tsx[103-107]

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 Journal light-mode muted text color does not provide sufficient contrast for normal-sized text.
## Issue Context
`rgba(34, 29, 23, 0.55)` over `#faf8f4` produces roughly 3.75:1 contrast, while Journal uses it for `text-sm` informational and navigational text. Choose a darker/less-transparent token that reaches at least 4.5:1 and add coverage for text-token contrast.
## Fix Focus Areas
- apps/self-hosted/src/styles/themes/journal.css[25-33]
- apps/self-hosted/src/themes/journal/journal-post-card.tsx[36-40]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[103-107]

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


8. Journal shell duplicates filter/label logic 🐞 Bug ⚙ Maintainability
Description
JournalShell reimplements availableFilters/currentFilter/filterLabel logic nearly verbatim from
BlogNavigation instead of extracting or reusing a shared hook, so any future bugfix or behavior
change to filter resolution (e.g. the post-filters hardening in PR #1156) must be applied twice and
can silently drift between themes.
Code

apps/self-hosted/src/themes/journal/journal-shell.tsx[R44-67]

+  const currentFilter = useMemo(() => {
+    const defaultFilter = availableFilters[0] || 'posts';
+    if (typeof location.search === 'string') {
+      return new URLSearchParams(location.search).get('filter') || defaultFilter;
+    }
+    if (
+      location.search &&
+      typeof location.search === 'object' &&
+      'filter' in location.search
+    ) {
+      return (location.search.filter as string) || defaultFilter;
+    }
+    return defaultFilter;
+  }, [location.search, availableFilters]);
+
+  // Mirrors blog-navigation's label resolution: an i18n key when one exists,
+  // a capitalized filter name otherwise.
+  const filterLabel = (filter: string): string => {
+    const key = `blog.navigation.${filter}`;
+    const translated = t(key as Parameters<typeof t>[0]);
+    return translated === key
+      ? filter.charAt(0).toUpperCase() + filter.slice(1)
+      : translated;
+  };
Relevance

●● Moderate

Refactor/extraction request is subjective; no close historical acceptance/rejection found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The currentFilter useMemo and filterLabel function in journal-shell.tsx duplicate the identical
currentFilter/getFilterLabel logic already implemented in blog-navigation.tsx, meaning the same
URLSearchParams parsing and i18n-key-fallback logic exists in two independently maintained files.

apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[24-76]
apps/self-hosted/src/themes/journal/journal-shell.tsx[44-67]

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

## Issue description
`JournalShell` duplicates the `currentFilter` resolution and `filterLabel`/`getFilterLabel` logic that already exists in `BlogNavigation`, instead of importing a shared implementation. This creates two independent copies of the same URL-parsing and i18n-fallback logic that must be kept in sync manually.
## Issue Context
Both components read `getConfiguredPostsFilters()` and then derive the active filter from `location.search`, with an identical fallback path for community/blog labels. Any future defect fix (e.g. malformed search param handling) applied to one copy will not automatically apply to the other.
## Fix Focus Areas
- apps/self-hosted/src/features/blog/layout/blog-navigation.tsx[24-76]
- apps/self-hosted/src/themes/journal/journal-shell.tsx[44-67]

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



Informational

9. Journal card omits image, no listType support 🐞 Bug ⚙ Maintainability
Description
JournalPostCard never renders a post's image even though the shared PostCard contract and
listType='grid' option imply images are part of the archive experience elsewhere in the app;
combined with declaring 'listType' unsupported, an owner cannot get any thumbnail on the Journal
archive even though other config (image proxy, post images) remains fully populated and functional
on the post page. This is by design per the PR description, so treat as informational rather than a
defect, but flag as a design/maintainability note since the manifest silently drops config the owner
may expect to still influence rendering (e.g. --theme-post-card-image-height/radius tokens declared
in journal.css are unused by JournalPostCard).
Code

apps/self-hosted/src/styles/themes/journal.css[R82-83]

+  --theme-post-card-image-height: 200px;
+  --theme-post-card-image-radius: 2px;
Relevance

●● Moderate

Design note; no clear precedent that unused theme tokens/images must be implemented for every theme.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
journal.css declares --theme-post-card-image-height and --theme-post-card-image-radius tokens (lines
82-83), part of the full theme-* contract, but JournalPostCard (journal-post-card.tsx) never renders
an <img> element or reads these tokens, leaving them dead in this theme.

apps/self-hosted/src/styles/themes/journal.css[82-83]
apps/self-hosted/src/themes/journal/journal-post-card.tsx[35-57]


10. Misplaced .test.ts in features 📜 Skill insight ⌂ Architecture
Description
New tests were added to a co-located *.test.ts file under src/features/, but tests must live
under the corresponding src/specs/ path and follow the .spec.ts/.spec.tsx naming convention.
Keeping tests in the wrong location/name breaks the required test organization standards and makes
discovery/enforcement inconsistent.
Code

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[R867-870]

+describe('theme-gated layout options', () => {
+  const fields = buildConfigFields((key) => key);
+  const layout = (fields.configuration as any).fields.instanceConfiguration.fields.layout.fields;
+
Relevance

● Weak

Exact precedent: test rename/move to src/specs + .spec.* was rejected in same file.

PR-#1443
PR-#1440

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a new describe block to
apps/self-hosted/src/features/floating-menu/config-fields.test.ts, which is both co-located under
src/features/ and uses a .test.ts filename, conflicting with the required src/specs/...
placement and .spec.* naming conventions.

apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-894]
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
Tests were added to `apps/self-hosted/src/features/floating-menu/config-fields.test.ts`, but compliance requires tests to be placed under the mapped `src/specs/...` directory and to use the `.spec.ts` / `.spec.tsx` naming pattern.
## Issue Context
This PR adds a new `describe('theme-gated layout options', ...)` block. Because it is being introduced/modified in this PR, it must comply with the test placement and naming rules.
## Fix Focus Areas
- apps/self-hosted/src/features/floating-menu/config-fields.test.ts[867-894]

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


Grey Divider

Context
Review mode: 🧠 Deep: This spans registry/API contracts, editor visibility behavior, search component resolution, and substantial new shell/card/CSS logic, creating multiple independent opportunities for subtle defects across paths.

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/journal/journal-shell.tsx
Comment thread apps/self-hosted/src/themes/journal/journal-shell.tsx
Comment thread apps/self-hosted/src/styles/themes/journal.css
feruzm added 2 commits August 12, 2026 10:54
The real catch: the default navigation is what mounts CreatePostButton,
so a shell that replaces it silently cost owners and community members
the way into the composer. JournalShell now mounts it, and that is the
standing contract for any theme shell.

Also from review: the filter state and label logic is extracted into a
shared usePostsFilterState hook consumed by both BlogNavigation and
JournalShell, so no shell can drift from the navigation's behavior; the
masthead action row wraps at narrow viewports instead of overflowing
the measure-width column; light-mode muted text rises to ~4.7:1 AA
over the paper background; and the visibility test walks the section
tree through a typed helper instead of an any cast.
No lint gate in this repo requires it today (verified: no stylelint
config exists), but two reviewers flagged the casing independently and
quoted strings are exempt from keyword-case rules, so a future
stylelint adoption cannot flag this file either.
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: Journal theme

1 participant