Skip to content

Conversation

@tvdeyen
Copy link
Member

@tvdeyen tvdeyen commented Nov 9, 2025

This PR migrates the admin sitemap from JavaScript to server-side rendering with custom elements and Turbo Streams.

Changes

  • Server-side rendering: Sitemap is now rendered server-side with Turbo Streams instead of JavaScript
  • Custom elements: Page nodes are rendered using ViewComponent-based custom elements
  • Preload entire page tree: The complete page tree is now preloaded in a single query, eliminating N+1 queries throughout the sitemap rendering
  • Performance improvements:
    • Extracted page tree loading into PageTreePreloader service class
    • Fixed N+1 queries in fold action (100+ individual queries → 1 bulk query)
    • Removed redundant FoldedPage queries in PageTreeSerializer
    • All page associations are preloaded upfront, preventing lazy loading queries
  • Code cleanup:
    • Removed unused admin pages tree action
  • Turbo Stream support: Added support for Turbo Stream responses in AJAX helpers

Benefits

  • Significantly better performance with eliminated N+1 queries
  • Simpler codebase with less JavaScript
  • More maintainable code with SRP-focused service classes
  • Progressive enhancement with custom elements

🤖 Generated with Claude Code

@tvdeyen tvdeyen requested a review from a team as a code owner November 9, 2025 20:22
@tvdeyen tvdeyen added this to the 8.1 milestone Nov 9, 2025
@codecov
Copy link

codecov bot commented Nov 9, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.18%. Comparing base (649a875) to head (b3ae52b).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3476      +/-   ##
==========================================
+ Coverage   97.16%   97.18%   +0.02%     
==========================================
  Files         285      287       +2     
  Lines        7466     7502      +36     
==========================================
+ Hits         7254     7291      +37     
+ Misses        212      211       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tvdeyen tvdeyen force-pushed the tree-node-component branch 3 times, most recently from c9ae856 to 3135053 Compare November 10, 2025 07:58
@tvdeyen tvdeyen force-pushed the tree-node-component branch 7 times, most recently from 67707f0 to 1adf4a8 Compare November 16, 2025 12:21
@@ -0,0 +1,159 @@
<li id="page_<%= @page.id %>" class="sitemap-item <%= @page.page_layout %>" data-slug="<%= @page.slug %>" data-restricted="<%= @page.restricted %>" data-page-id="<%= @page.id %>" data-folded="<%= @page.folded?(current_alchemy_user&.id) %>">
Copy link
Contributor

Choose a reason for hiding this comment

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

This partial is huge. Maybe we extract some things into smaller partials to make it easier to read?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. I am a bit uncertain, because every render call in Rails is very costly. It has been rendered front end side because of this. Rendering is already the largest part of the response (500ms vs 30ms DB for 1000 pages ten levels deep)

Need to do some benchmarking first.

<% if can?(:edit_content, @page) %>
<span class="page-icon<%= @page.root? ? '' : ' handle' %>">
<% if @page.locked? %>
<sl-tooltip content="<%= Alchemy.t("This page is locked", name: @page.locker_name) %>" class="like-hint-tooltip" placement="bottom-start">
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add an icon with tooltip helper / component? On this partial, I think there's five instances of disable_sitemap_tool(icon_name:, tooltip:)

<div class="sitemap_tool disabled">
  <sl-tooltip content="<%= tooltip %>" class="like-hint-tooltip" placement="bottom-start">
    <%= render_icon(icon_name) %>
  </sl-tooltip>
</div>

Copy link
Member Author

Choose a reason for hiding this comment

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

See above. Let's do some benchmarking

</div>
<div class="page_infos">
<% if @page.locked? %>
<span class="page_status locked">
Copy link
Contributor

Choose a reason for hiding this comment

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

3 instances of page_status_icon

<span class="page_status">
  <alchemy-icon name="<%= icon_name %>" size="1x"></alchemy-icon>
  <%= status_text %>
</span>

Copy link
Member Author

Choose a reason for hiding this comment

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

making even more render calls is exponentially increasing the server render times. Will do some benchmarking

tvdeyen and others added 13 commits November 26, 2025 18:54
Eager load all pages and associations and render
the whole page tree on the server via a view component.

Skipping folded pages for current user.
Useful for handling turbo stream responses in JS code
Replace the Handlebars-based sitemap implementation with
web components and use Turbo Streams to replace the
children.
Extract Page.preload_sitemap into PageTreePreloader service class
following Single Responsibility Principle.

This also fixes N+1 queries in the fold action by preloading the
:folded_pages association in Page#preloaded_children, reducing 100+
individual queries to a single bulk query when unfolding pages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Use a page to start from loading the page tree and
return that page with preloaded children. Easier API
then the double-state from before.

Signed-off-by: Thomas von Deyen <[email protected]>
Current LTS

Signed-off-by: Thomas von Deyen <[email protected]>
Reduce attributes we do not use

Signed-off-by: Thomas von Deyen <[email protected]>
Thanks Herb :)

Signed-off-by: Thomas von Deyen <[email protected]>
the sitemap should not now about how to handle the folder
icon and button

Signed-off-by: Thomas von Deyen <[email protected]>
Using a mutation oberserver to watch for child
mutations and reinit Sortable after a page was
unfolded and children got replaced.

Signed-off-by: Thomas von Deyen <[email protected]>
Only load pages that do not have a folded parent.
This will drasticly reduce the amount of associated
records that need to be preloaded for large trees
with lots of folded page tranches.

Signed-off-by: Thomas von Deyen <[email protected]>
If someone wants to adjust how we preload whole
page trees they can confgure a different class.

Signed-off-by: Thomas von Deyen <[email protected]>
@tvdeyen tvdeyen force-pushed the tree-node-component branch from 1adf4a8 to b3ae52b Compare November 26, 2025 17:57
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.

4 participants