The app concept: what a reader sees, and where the index lives

"systematically (default) would appreciate side menu rather than bottom nodes… we prefer index on side… and yes, there will be exceptions… try to work through app concept in general." — maintainer, 2026-09-17

Status 2026-10-11 — the missing rail is closed. Maintainer: "the default layout should NOT contain this kind of view of children at the bottom. we should use the more explorer-like navigation … combine this with mesh search … folder-like nav … don't eager load the whole thing … load max 50 items … and load more."

The sections below are the analysis that led here. The app-root resolution (index root = nearest app root, not always the first path segment) is still open.

The immediate change (the index moves to the side rail, the bottom catalog goes) is simple. Applying it exposed something larger: an app has no index at all, and the contents catalogs scattered through this repo's plugin pages are the workaround. This page states the concept, names where the current surfaces disagree, and recommends the one rule that would make them agree.

What an app IS today

An app is a tile whose App.Plugin names the path of a node — "usually the Store plugin cover (e.g. Chess, LinkedIn)". The record lives at {user}/_App/{appId} as an ordinary mesh node; the grid is a single-partition query over {owner}/_App, and the tile's name and icon resolve from the app's root node. Default apps are materialized write-behind from Admin/HomeConfig.DefaultApps on home render.

So: an app is a node you open. There is no "app shell", no app-level container type, and nothing that says "these pages belong to this app" beyond the path prefix.

Three navigation models, and where they stop

Surface Who supplies it What it answers
The launcher — home Apps grid, icon tiles grouped by category InstalledApp records + Admin/HomeConfig.DefaultApps Which apps do I have?
A markdown tree — the left-hand rail DefaultNodeNavigation, rooted at the page's SPACE Where am I in this document?
Inside a typed app — a Store/Plugin root, a mailbox, a CRM client nothing —

The third row is the gap, and it is not cosmetic:

The measured consequence. 78 node bodies in this repo end in a contents catalog. 54 of them are Store/Plugin roots — app front pages — and their catalog is the only child navigation they have. Removing it would leave those apps with no way in. That is why the 2026-09-17 sweep touched only the 24 Markdown bodies (21 changed, 3 kept as genuine registers) and left the plugin roots alone. The catalogs are a symptom; the missing rail is the defect.

Where the conventions disagree

  1. Two orderings. The rail orders by MeshNode.Order, then name, recursively. The launcher orders by App.Order with 0 meaning "never placed" (painting behind explicitly ordered tiles, most-recently-used first) and groups by category. A reader moving from grid to rail meets two different ideas of sequence.
  2. Two ideas of "the root". DefaultNodeNavigation.IndexRoot is the first path segment — deliberately the space, so one index serves the whole tree. But an app installed into a user home lives at {user}/…, so its index root resolves to the user's home, not the app. An app's index would show the home's tree with the app as one entry inside it.
  3. Embeds get no rail, correctly — showHeader=false suppresses it and providers are not even asked. Worth keeping, and worth stating: an @@ embed is content, never a page.
  4. The user home is the one typed page that solved this, by embedding @@("area/Catalog") — the Apps grid. That is the correct exception (the tiles are the content), and it is also evidence that the typed-page answer has so far been "hand-roll a region in the body".

The recommendation

One rule: the index is a property of the NODE, not of the markdown renderer.

  1. Give typed nodes the same default rail. Have AddDefaultLayoutAreas() wrap Overview with the navigation MarkdownOverviewLayoutArea already builds — the same SuppliedNavigationRail, the same supplied-then-default precedence, the same two guards (emit at once; a faulted query renders no index rather than holding the page). A Store/Plugin, a mailbox and a CRM client then each get an index for free.
  2. Resolve the index root to the nearest APP ROOT, not the first path segment. An app root is a node that owns a partition, or one an InstalledApp record points at. Chess keeps today's behaviour; a course installed at {user}/AgenticEngineering starts its index at the course instead of the reader's whole home. This is the change that makes rule 1 useful rather than noisy.
  3. Keep INodeNavigationProvider as the override, unchanged. A module that knows its own semantics (a course lists the whole course, not the branch you stand in) still wins over the default. The default exists so that not implementing the seam is no longer the same as having no navigation.
  4. Then retire the 54 catalogs in plugin bodies, leaving the genuine registers. Not before: the sweep is only safe once the rail is there, which is the ordering this page exists to record.

The author-facing rule, once that lands: would a reader look for this in the side rail? Then it belongs in the rail. Embed a catalog only where the listing IS the content — a log, a register, a store page, or a filtered view the rail cannot express (?groupBy=, ?subtree=true). Give it a heading that says what it is (## Correspondence, ## Releases), never a bare ## Contents.

The exceptions, declared: what a Space page can turn off

One exception is DECLARED on the node — MeshNode.ExcludeFromContext, core's MeshNodeVisibility.IsExcludedFromContext, the same mechanism a node already uses to stay out of search or a create menu:

Declaration What the Space page drops Read by
ExcludeFromContext: ["header"] (MeshContexts.Header) the logo tile, the click-to-edit title, the description, the divider SpaceLayoutAreas.BuildSpaceView

The header additionally honours ?showHeader=false on the area reference — what an @@ embed and the landing-page route (PublicPagePresentation) put there — because an embed is content, never a page.

Author it wherever the node is authored. In a node JSON, "excludeFromContext": ["header"]; in markdown front matter, ExcludeFromContext: [header]; in code, node.HideFrom(MeshContexts.Header). The value is a lower-case wire identifier and is never translated.

🚨 Per INSTANCE only — a declaration on the NodeType does nothing here. MeshNodeVisibility.IsExcludedFromContext reads the node's own ExcludeFromContext (plus the dotfile rule for search); the TYPE-level exclusion is the query layer's, from MeshConfiguration.GetExcludedNodeTypes, which a layout area never consults.

Retired: the contents opt-out

The Space page no longer appends a catalog, so there is nothing to opt out of. The switch ExcludeFromContext: ["contents"], the body scan that looked for an existing catalog embed, and the method that combined the two are removed with it. SpaceLayoutAreas.ContentsContext remains as a constant so a node that still declares the value keeps loading; the declaration has no effect and can be deleted from a node whenever it is next edited. An HTML comment carrying area/Search, which some pages used to satisfy the old scan, is likewise inert and can go.

A page that wants a listing in its body embeds one explicitly, under a heading that says what it is.

Owed to the platform (core): SpaceNodeType.WelcomeMarkdown still ends with ## Contents and an @@("area/Search") embed, which restates the rail on every new Space; it is removed in a core change after the rail is released.

Follow-ups

# Item Status
1 Typed nodes render the default rail Done in core (MarkdownOverviewLayoutArea.WithIndexRail wraps the typed default Overview, MeshWeaver#6472). It reaches an instance with the platform release that contains it; until an instance runs that release its typed pages have no rail
2 IndexRoot → nearest app/partition root Changes the index of every nested page; wants tests over the installed-course and user-home shapes before it ships
3 Sweep the 54 Store/Plugin catalogs No longer blocked by design. Do it once item 1 is released to the instances the packages ship to — removing a catalog on an instance whose typed pages still have no rail strips that app's only navigation
4 Reconcile the two orderings (rail Order+name vs launcher App.Order+category) Design question: does an app declare its own section order, and does the rail honour categories?

What changed on 2026-09-17