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
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:
MarkdownOverviewLayoutAreawraps its page with the rail.MeshNodeLayoutAreas.AddDefaultLayoutAreas()— what every typed NodeType uses — contains no reference to the rail at all.App.Pluginpoints at aStore/Plugincover. So the node an app opens onto is exactly the kind of node that renders no index. A reader arrives from the launcher and the app has no visible structure.- The escape hatch is
INodeNavigationProvider, which core prefers over its default. Exactly one module uses it — Education, for course pages — and its doc comment explains why: core must not learn what a lesson is. That reasoning is right, and it is an argument for a seam, not for core having no default for typed nodes.
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
- Two orderings. The rail orders by
MeshNode.Order, then name, recursively. The launcher orders byApp.Orderwith0meaning "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. - Two ideas of "the root".
DefaultNodeNavigation.IndexRootis 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. - Embeds get no rail, correctly —
showHeader=falsesuppresses it and providers are not even asked. Worth keeping, and worth stating: an@@embed is content, never a page. - 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.
- Give typed nodes the same default rail. Have
AddDefaultLayoutAreas()wrapOverviewwith the navigationMarkdownOverviewLayoutAreaalready builds — the sameSuppliedNavigationRail, the same supplied-then-default precedence, the same two guards (emit at once; a faulted query renders no index rather than holding the page). AStore/Plugin, a mailbox and a CRM client then each get an index for free. - 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
InstalledApprecord points at.Chesskeeps today's behaviour; a course installed at{user}/AgenticEngineeringstarts its index at the course instead of the reader's whole home. This is the change that makes rule 1 useful rather than noisy. - Keep
INodeNavigationProvideras 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. - 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
The rule above ends "and yes, there will be exceptions". Two of them are now DECLARED rather than
inferred, and both are properties of 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 |
ExcludeFromContext: ["contents"] (SpaceLayoutAreas.ContentsContext) |
the default bottom Contents catalog | SpaceLayoutAreas.TakesDefaultBottomCatalog |
They are independent by design: a marketing landing carries its own cover AND is its own catalogue,
but a Space with a hand-made cover may still want the listing, and a Space that wants no listing may
still want its title. 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. The catalog has no reference channel, deliberately: one mechanism until
something needs a second.
Author it wherever the node is authored. In a node JSON,
"excludeFromContext": ["contents"]; in markdown front matter, ExcludeFromContext: [contents]; in
code, node.HideFrom(SpaceLayoutAreas.ContentsContext). 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. That is also the
behaviour you want: the only NodeType a Space instance has is Space itself, so a type-level
contents exclusion would switch the default catalogue off for EVERY Space in the mesh — the opposite
of a per-Space presentation switch.
Why the catalog opt-out had to exist
TakesDefaultBottomCatalog otherwise answers the question by REGEX over the body
(CatalogEmbedRegex), and that conflates two different questions:
- does this body already show a catalog? — a de-duplication question, and for authored markdown a text search really is the only way to answer it. It stays.
- does this author WANT one? — a declaration, and there was none. With only the first, refusing the
catalog meant satisfying the regex without rendering anything: an HTML comment carrying the
words
area/Search. The public landing Space shipped exactly that (Memex#463), against a private regex it could not see, and the platform's ownSpaceNodeType.WelcomeMarkdowndoc comment promised the opposite — that an author "can move, tune … or remove it like any other content", which deleting the embed does not achieve, because a body carrying none is then given the default at the bottom. Filed as #2221.
Owed to the platform (core, not this repo): that doc comment is still wrong, and should name this
opt-out. And core 4e7e9fd182 ("the welcome page stops restating the index") was REVERTED the same
day by core 69c23c62 — "until the Plugins half can land" — so on today's core the welcome
template embeds @@("area/Search") again; the Plugins half has landed, so the core half can re-land.
SpaceDefaultCatalogTest.DefaultWelcomeTemplate_TakesNoBottomCatalog_OnEitherPlatform is pinned to
the OUTCOME precisely so it holds on either side of that.
Owed to the estate: the public landing Space (Memex → mesh/Home/index.json) can declare the
context and delete its comment hack; both are one line.
Follow-ups
| # | Item | Why it is not done here |
|---|---|---|
| 1 | Typed nodes render the default rail (AddDefaultLayoutAreas) |
Core change touching every typed page in the fleet; needs visual verification, which this repo has no local path for (the portal ships in the image) |
| 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 |
Blocked on 1 — removing them today strips an 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
src/MeshWeaver.AI/Data/Skill/space.md§4 rewritten: the rail is the index; the exception test is stated; the typed-node caveat is explicit.src/MeshWeaver.AI/Data/Skill/markdown.mdandsrc/MeshWeaver.AI/Data/Agent/Assistant.md: the same rule, so the default agents stop teaching the old one.- 21
Markdownnode bodies lost their trailing catalog;Governance/Standards,Governance/ActivitiesandPublish/Deckskept theirs (the listing is the content). - Core: the Space welcome template no longer ends in
@@("area/Search")(MeshWeaver#4586) — 🚨 and that was REVERTED the same day by core69c23c62, "until the Plugins half can land", so the template embeds it again on today's core. The line above stood as fact here for four days; read the pin, never this page, for what the template currently says.