Definition
This is the pattern you want when building a UI like Notion or a wiki, where "a document contains other documents, and you keep drilling in by clicking them." Hierarchical document navigation deals not with a single flat document but with a graph in which documents reference one another. You move between child documents with page blocks (blocks that point to another document from within a document) and a navigator (the interface responsible for navigation actions: push = go in, peek = glance via a preview window, back = go back). A breadcrumb (a current-location path display like "Home > Projects > Details") reflects the stack you have entered so far, and a peek dialog (a modal window for peeking) reuses the readOnly editor surface (the editor screen in a non-editable mode).
Understanding it in context
When a document is a single layer, like one blog post, URL routing alone is enough. But in a structure where "opening a project reveals child documents inside it, and opening those reveals even deeper documents," swapping the whole page each time breaks the context. This pattern views documents as a graph connected by ids, and maintains navigation continuity by swapping only the content id inside the same modal shell (peek), or pushing onto a stack to go deep (push) and then popping back out (back). The key is not "browser back" but an explicit stack managed by the app itself.
Why it matters
Wiki·portfolio·KB demos become convincing only when there is depth like "Home→Projects→Details." URL routes alone make it hard to create overlay peek·in-place push continuity.
How it works
- A page block references a
documentId— distinguish links (route) from embeds (in-place). - The navigator provides a
{ push, peek, back }contract — as an injectable interface rather than React context, so Storybook·tests can use an in-memory graph. - Peek keeps the modal shell and swaps only the inner id — reusing the readOnly editor surface to prevent markup drift.
- Archive collects subtree page ids at an applyPatches checkpoint and passes them to the repository — bundling "delete block" and "clean up child documents" into one transaction.
- The document resolver does per-id fetch dedup + blocks the (newId, oldContent) intermediate frame (render-time reset or
key={id}).
Practical application
- Keep the navigator as an injectable interface so Storybook·tests use an in-memory graph.
- Put the slash command "Page" behind a capability gate.
- After a successful create, bundle the block patch and document create into one user action.
Trade-offs
- In-modal push is separate from route VT — explicit stack pop instead of
router.back(). - The resolver cache raises performance↑ but stale bugs↑ — key reset on id change is essential.
When not to use it
- If depth=1, like a single-page blog, page blocks·peek are overkill.
Common mistakes
- Mounting a new Dialog per peek → animation/focus breaks.
- refresh being cache-first, so reopen shows the previous revision.
Related concepts
- focused-guest-editor — the peek editing surface
- radix-portal-scoped-css — isolating dialog styles