Skip to content

fix: eliminate layout shifts on public wiki page load#595

Merged
NagariaHussain merged 2 commits intodevelopfrom
fix/layout-shifts
Apr 6, 2026
Merged

fix: eliminate layout shifts on public wiki page load#595
NagariaHussain merged 2 commits intodevelopfrom
fix/layout-shifts

Conversation

@NagariaHussain
Copy link
Copy Markdown
Collaborator

@NagariaHussain NagariaHussain commented Apr 6, 2026

Summary

  • Sidebar width: Set default CSS width (w-72) and read persisted collapse state from localStorage in a blocking <script> before render — same pattern as the existing theme initialization
  • Sidebar tree: Compute ancestor nodes server-side, pass to template, use x-cloak on non-ancestor children so they start hidden instead of flashing visible then collapsing
  • Content area: Remove max-width from transition — the ch unit changes when the custom font loads via font-display: swap, causing a visible 300ms animated width shift
  • Search shortcut: Add default ⌘K text so the kbd element isn't empty before Alpine loads

Closes #591

Summary by CodeRabbit

  • Bug Fixes

    • Prevented sidebar layout shift/flash on initial load by initializing collapsed state earlier.
  • Improvements

    • Persist sidebar collapsed state more reliably across sessions.
    • Made sidebar width explicit and added a dedicated toggle styling hook.
    • Adjusted page transition behavior for smoother opacity transitions.
    • Search shortcut display now shows a default hint while honoring platform-specific text.
    • Navigation tree now respects pre-expanded nodes for initial rendering.

Three root causes of CLS (Cumulative Layout Shift) on the Jinja-rendered
wiki pages:

1. Sidebar width was entirely set by Alpine's :style binding — before
   Alpine loaded, the sidebar had no width, causing content to reflow.
   Fixed by adding w-72 as default CSS width and a blocking script that
   reads the persisted collapse state from localStorage (same pattern as
   the existing theme initialization).

2. Sidebar tree children were all visible in raw HTML, then collapsed by
   Alpine, then re-expanded for ancestors — two sequential shifts. Fixed
   by computing ancestor nodes server-side and passing them to the
   template, which uses x-cloak on non-ancestor children so they start
   hidden.

3. Content area had transition-[max-width] with max-w-[80ch]. The ch
   unit depends on font glyph width — when InterVar loaded via
   font-display:swap, the ch value changed and the transition animated
   the content width over 300ms. Fixed by transitioning only opacity.

4. Search shortcut kbd was empty until Alpine's x-text filled it in.
   Fixed by adding default text content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 832bd704-5965-44ad-8984-9308b7c47d4b

📥 Commits

Reviewing files that changed from the base of the PR and between e2bceee and 1886d0a.

📒 Files selected for processing (2)
  • wiki/templates/wiki/includes/sidebar.html
  • wiki/templates/wiki/layout.html

Walkthrough

This pull request adds server-side computation of an expanded_nodes set and includes it in the wiki page template context. The sidebar tree macro now accepts an expanded_nodes parameter and uses it to control initial group visibility. Sidebar collapse state is read from localStorage during initial render and persisted via Alpine, with an init hook removing a pre-render attribute. Templates update layout/CSS (article transition and sidebar sizing), add a wiki-sidebar-toggle class, and adjust the search shortcut markup.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: eliminate layout shifts on public wiki page load' accurately summarizes the main objective of the PR, which addresses cumulative layout shifts on wiki pages through multiple targeted fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
wiki/templates/wiki/includes/sidebar.html (1)

313-316: Remove debug code before merging.

Lines 314-316 contain debug logging that should be removed:

this.$nextTick(() => {
    const allRouteElements = document.querySelectorAll('[data-route]');
});

This queries the DOM but doesn't use the result—appears to be leftover debug code.

♻️ Proposed fix
 init() {
     // Auto-expand parents of current page
     this.expandCurrentPageParents();
-    
-    // Debug: log all route elements
-    this.$nextTick(() => {
-        const allRouteElements = document.querySelectorAll('[data-route]');
-    });
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wiki/templates/wiki/includes/sidebar.html` around lines 313 - 316, Remove the
leftover debug DOM query: delete the this.$nextTick(...) block that declares the
unused allRouteElements constant (the snippet starting with "this.$nextTick" and
"const allRouteElements = document.querySelectorAll('[data-route]');"); if you
intended to use the nodes, replace the block with the actual logic referencing
allRouteElements inside the same this.$nextTick callback, otherwise remove the
entire callback to avoid dead code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wiki/templates/wiki/includes/sidebar.html`:
- Around line 54-59: The persisted key for the sidebar collapse state is missing
an explicit name; update the Alpine store definition (Alpine.store('sidebar'))
so the isCollapsed property uses an explicit persist key by changing
isCollapsed: Alpine.$persist(false) to use .as(...) with the matching key used
by the pre-Alpine script (e.g., .as('sidebar.isCollapsed')), leaving the init()
method that removes data-sidebar-collapsed unchanged.

In `@wiki/templates/wiki/macros/sidebar_tree.html`:
- Around line 54-59: The sidebar can briefly collapse on first render because
expandCurrentPageParents() sets expandedStates in $nextTick; initialize
expandedStates synchronously during wikiSidebar.init() instead so the
server-provided expanded_nodes map is applied before Alpine evaluates x-show;
locate wikiSidebar.init(), expandCurrentPageParents(), and the expandedStates
object and ensure expandedStates is populated from the server-side
expanded_nodes (and respects node IDs used in isExpanded('{{ node.name }}'))
immediately rather than inside a $nextTick callback, removing the timing gap
that causes the flash while keeping x-cloak usage for nodes not in
expanded_nodes.

---

Nitpick comments:
In `@wiki/templates/wiki/includes/sidebar.html`:
- Around line 313-316: Remove the leftover debug DOM query: delete the
this.$nextTick(...) block that declares the unused allRouteElements constant
(the snippet starting with "this.$nextTick" and "const allRouteElements =
document.querySelectorAll('[data-route]');"); if you intended to use the nodes,
replace the block with the actual logic referencing allRouteElements inside the
same this.$nextTick callback, otherwise remove the entire callback to avoid dead
code.
🪄 Autofix (Beta)

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

Run ID: 7937a40d-44c3-4184-934d-d2afa965b3ee

📥 Commits

Reviewing files that changed from the base of the PR and between b7f8700 and e2bceee.

📒 Files selected for processing (6)
  • wiki/frappe_wiki/doctype/wiki_document/wiki_document.py
  • wiki/templates/wiki/document.html
  • wiki/templates/wiki/includes/header.html
  • wiki/templates/wiki/includes/sidebar.html
  • wiki/templates/wiki/layout.html
  • wiki/templates/wiki/macros/sidebar_tree.html

Comment thread wiki/templates/wiki/includes/sidebar.html
Comment on lines 54 to +59
<div id="{{ node.name }}-children"
class="wiki-children overflow-hidden mt-0.5 ml-[14px] pl-2 border-l border-[var(--outline-gray-2)]"
x-show="isExpanded('{{ node.name }}')"
x-collapse>
{{ render_wiki_tree(node.children, level + 1, current_route) }}
x-collapse
{% if node.name not in expanded_nodes %}x-cloak{% endif %}>
{{ render_wiki_tree(node.children, level + 1, current_route, expanded_nodes) }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential flash for ancestor nodes on first visit.

The logic correctly uses server-computed expanded_nodes to avoid x-cloak on ancestor nodes. However, there's a timing concern:

  1. Ancestor nodes render visible (no x-cloak)
  2. Alpine initializes, x-show="isExpanded(...)" evaluates to false (empty expandedStates)
  3. expandCurrentPageParents() runs in $nextTick, setting expandedStates[nodeId] = true

Between steps 2 and 3, ancestor nodes might briefly collapse before re-expanding. The x-collapse directive may mitigate this by deferring the animation, and on subsequent visits expandedStates is persisted so this only affects first-time visitors.

Consider initializing expandedStates synchronously (not in $nextTick) in wikiSidebar.init() to eliminate any potential flash.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wiki/templates/wiki/macros/sidebar_tree.html` around lines 54 - 59, The
sidebar can briefly collapse on first render because expandCurrentPageParents()
sets expandedStates in $nextTick; initialize expandedStates synchronously during
wikiSidebar.init() instead so the server-provided expanded_nodes map is applied
before Alpine evaluates x-show; locate wikiSidebar.init(),
expandCurrentPageParents(), and the expandedStates object and ensure
expandedStates is populated from the server-side expanded_nodes (and respects
node IDs used in isExpanded('{{ node.name }}')) immediately rather than inside a
$nextTick callback, removing the timing gap that causes the flash while keeping
x-cloak usage for nodes not in expanded_nodes.

The blocking script and Alpine store were coupled via Alpine's implicit
key format (_x_isCollapsed). Use an explicit .as('sidebar.isCollapsed')
key so the two stay in sync regardless of property renames or Alpine
internals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NagariaHussain NagariaHussain merged commit 0421984 into develop Apr 6, 2026
5 checks passed
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.

Unnecessary growing transition on first load

1 participant