diff --git a/client/src/entry.jsx b/client/src/entry.jsx
index 1c10dc5..ef9177d 100644
--- a/client/src/entry.jsx
+++ b/client/src/entry.jsx
@@ -194,6 +194,14 @@ registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep)
// enough; on a core that knows nothing of Teams it simply stays empty.
registry.declareModuleSlot(ID, 'uo.guild.detail')
+// A SECOND place on the same page, for core's Team forum (TEAMS.md Part 5). Two
+// declarations rather than one, because a slot holds one component and this module
+// wants to decide where each of core's two contributions sits on its own page —
+// the feed reads as part of the guild's story, the forum is a room you go into.
+// Neither knows the other exists, and a core that fills only one leaves the other
+// empty.
+registry.declareModuleSlot(ID, 'uo.guild.forum')
+
// `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 91ce62c..945cc21 100644
--- a/client/src/routes/public/Guild.jsx
+++ b/client/src/routes/public/Guild.jsx
@@ -102,6 +102,11 @@ export default function Guild() {
that does not fill it, or when there is nothing to show. The guild
is named in OUR terms — core maps its own Team from these two. */}
+
+ {/* And the Team forum, in its own place below the feed. Core resolves
+ who may read it — membership and manual grants are core's rules —
+ so this module renders the room and never its door policy. */}
+
>
)}
diff --git a/client/test/registration.test.js b/client/test/registration.test.js
index f3a124c..5378bea 100644
--- a/client/test/registration.test.js
+++ b/client/test/registration.test.js
@@ -209,16 +209,22 @@ it('registers under exactly one module id, matching the manifest', () => {
assert.deepEqual([...owners], [manifest.id])
})
-it('declares its own guild-detail slot, for core to fill', () => {
+it('declares its own guild slots, for core to fill', () => {
// The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no
- // core page: core owns the activity feed and this module owns the word "guild",
- // so this module declares the place and core puts the feed in it.
- assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail'])
+ // 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'])
})
-it('the declared slot is rendered by the page that owns it', () => {
+it('every declared slot is rendered by the page that owns it', () => {
// A slot nothing renders is a slot core fills into the void. Asserted against
// the source rather than the chunk, since the chunk is minified.
const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8')
- assert.match(page, /name="uo\.guild\.detail"/)
+ for (const name of registered.declaredSlots) {
+ assert.match(page, new RegExp(`name="${name.replace(/\./g, '\.')}"`))
+ }
})