If you check @angus Custom Wizard Plugin topic, as an anonymous user, nested replies view: https://meta.discourse.org/n/-/73345.json?page=0&sort=old
/n/-/73345.json?page=0&sort=old comes back with 20 roots and every single one is a deleted placeholder — empty cooked, no author, total_descendant_count: 0. So the whole first page is blank “removed” rows.
The flat view of the same posts shows nothing at all (post #2 just skips to related topics), so these are correctly hidden there. The nested view keeps them and counts them toward the page, which is the mismatch.
Looks like the tree loader pulls in deleted posts on purpose (to keep a deleted parent visible when it still has live replies under it), but it’s also keeping deleted posts that have no visible replies left. On a topic like this where the early replies were all deleted, you end up with a full page of empty placeholders instead of actual content.
Setting’s definitely on — the endpoint returned 200 instead of 404.
The culprit looks like apply_visibility in NestedReplies::TreeLoader — the scope.unscope(where: :deleted_at) pulls every deleted post into the tree. Makes sense for a deleted parent that still has live replies under it, but it’s also keeping deleted leaves and fully-deleted branches, and those eat slots in the ROOTS_PER_PAGE window (and count toward has_more_roots).
For non-staff I think you want to drop a deleted post unless it’s got at least one non-deleted descendant — keep it only when its visible total_descendant_count > 0. NestedViewPostStat already excludes deleted/whisper descendants from that count, so there’s no extra query needed.
Would need to apply in the three spots that build the page so you don’t get short pages: root_posts_scope, batch_preload_tree, and the page-fill / has_more_roots bit. Staff path stays as-is for recovery.

