diff --git a/client/src/entry.jsx b/client/src/entry.jsx
index ef9177d..90209b3 100644
--- a/client/src/entry.jsx
+++ b/client/src/entry.jsx
@@ -202,6 +202,13 @@ registry.declareModuleSlot(ID, 'uo.guild.detail')
// empty.
registry.declareModuleSlot(ID, 'uo.guild.forum')
+// And a THIRD, at the top of the same page, for core's per-Team notification
+// control (TEAMS.md §6.3). Same reasoning as the other two and a different place:
+// muting a guild is an action ON this page, so it sits with the page's heading
+// rather than after its content. Core resolves whether this viewer is in the
+// Team at all — this module neither knows nor asks.
+registry.declareModuleSlot(ID, 'uo.guild.header')
+
// `module.json`'s `coreApi` range is checked by the loader before this file is
// ever served, so there is nothing to re-check here. It is logged because a
// mismatch between the core that validated the manifest and the core that
diff --git a/client/src/routes/public/Guild.jsx b/client/src/routes/public/Guild.jsx
index 945cc21..eea0df8 100644
--- a/client/src/routes/public/Guild.jsx
+++ b/client/src/routes/public/Guild.jsx
@@ -75,6 +75,13 @@ export default function Guild() {
{data.alliance && ` · ${data.alliance}`}
+ {/* A third place for core, up here rather than below the roster: core
+ puts this guild's notification control in it, and a control that
+ acts on the page belongs beside the page's title and not after its
+ content. Empty for a visitor with no membership, and on a core
+ that fills nothing. */}
+
+
{roster.length > 0 && (
diff --git a/client/test/registration.test.js b/client/test/registration.test.js
index 5378bea..9e41927 100644
--- a/client/test/registration.test.js
+++ b/client/test/registration.test.js
@@ -214,10 +214,16 @@ it('declares its own guild slots, for core to fill', () => {
// core page: core owns the activity feed and the forum, this module owns the
// word "guild", so this module declares the places and core puts them in.
//
- // TWO slots rather than one because a slot holds one component: stacking the
- // feed and the forum into a single fill would take away this module's ability
- // to place them separately on its own page.
- assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail', 'uo.guild.forum'])
+ // THREE slots rather than one because a slot holds one component: stacking the
+ // feed, the forum and the notification control into a single fill would take
+ // away this module's ability to place them separately on its own page — and it
+ // does place them separately, the control above the roster and the other two
+ // below it.
+ assert.deepEqual([...registered.declaredSlots], [
+ 'uo.guild.detail',
+ 'uo.guild.forum',
+ 'uo.guild.header',
+ ])
})
it('every declared slot is rendered by the page that owns it', () => {
diff --git a/server/model/teamProvider/teamProvider.model.js b/server/model/teamProvider/teamProvider.model.js
index 8b5b76b..4196520 100644
--- a/server/model/teamProvider/teamProvider.model.js
+++ b/server/model/teamProvider/teamProvider.model.js
@@ -317,4 +317,18 @@ async function projectRoster(externalId, members, viewer) {
}
}
-module.exports = { getTeams, getTeamMembers, getTeamLeaders, projectRoster, boardIsCurrent }
+// Where core should point a link at a guild (MODULE_API 1.6.0, TEAMS.md §6.4).
+//
+// **Core cannot work this out for itself, and it is not supposed to.** Teams are
+// a contract primitive with no core surface — this module owns the guild page,
+// because core does not own the word "guild" — so the one thing core needs back
+// is where the page it does not own actually lives. A notification email that
+// cannot link to the thread it is about is most of the way to useless.
+//
+// A relative path with `{externalId}` substituted, matching `Guild.jsx`'s route
+// (`/uo/guilds/:id`). Core does the substitution and nothing else with it; a
+// template naming its own host is refused at registration, which is why this is
+// data and not a callback.
+const pageUrlTemplate = '/uo/guilds/{externalId}'
+
+module.exports = { getTeams, getTeamMembers, getTeamLeaders, projectRoster, boardIsCurrent, pageUrlTemplate }