-
{t('nav.members')}
-
{t('nav.stats')}
-
{t('nav.events')}
-
{t('nav.tools')}
-
{t('nav.builders')}
+ {nav.members &&
{t('nav.members')}}
+ {nav.stats &&
{t('nav.stats')}}
+ {nav.events &&
{t('nav.events')}}
+ {nav.tools &&
{t('nav.tools')}}
+ {nav.builders &&
{t('nav.builders')}}
{t('nav.about')}
{alternates.length > 1 && (
diff --git a/src/lib/atproto/home.ts b/src/lib/atproto/home.ts
index 936393f..e105020 100644
--- a/src/lib/atproto/home.ts
+++ b/src/lib/atproto/home.ts
@@ -1,9 +1,9 @@
import { getListMembers, getProfiles, getFeed } from './client';
import { mapLimit } from './activity';
import { getSifaInfo, type SifaInfo } from './sifa';
-import { memberDids, beMemberDids, huMemberDids, memberListUri, memberBlocklist, COMMUNITY_DID } from '../../data/members';
+import { memberBlocklist, COMMUNITY_DID } from '../../data/members';
import { getSelfAssertedMembers } from './member-index';
-import { getScope } from '../../config/scopes';
+import { getScope, SCOPES, type Scope } from '../../config/scopes';
import { matchNlCity } from '../../data/nl-cities';
import { feedForScope } from '../../data/feed';
import { seededShuffleInPlace } from '../seeded-shuffle';
@@ -37,26 +37,24 @@ async function loadCommunity(): Promise {
const scope = getScope(import.meta.env.PUBLIC_SITE_SCOPE);
const selfAsserted = await getSelfAssertedMembers(COMMUNITY_DID);
const blocked = new Set(memberBlocklist);
- // Scope the member set: the .eu aggregate shows everyone (the NL seed + all self-asserted); a
- // national site shows self-asserted members whose record claims its country, plus the NL seed on
- // .nl. .be starts empty and fills as people join on atproto.be.
+ // The shown members are a dedup-by-DID union of three sources per country (see
+ // decisions/2026-09-21-centralized-country-config.md): the scope's managers + its manual
+ // memberSeed + self-asserted `eu.atcommons.member` holders whose record claims the country. The
+ // .eu aggregate unions every national scope's seed with all self-asserted members.
+ const seedOf = (s: Scope): string[] => [...(s.managers ?? []), ...(s.memberSeed ?? [])];
let didList: string[];
if (scope.aggregate) {
- didList = [...memberDids, ...beMemberDids, ...huMemberDids, ...selfAsserted.map((m) => m.did)];
+ const seed = Object.values(SCOPES).filter((s) => !s.aggregate).flatMap(seedOf);
+ didList = [...seed, ...selfAsserted.map((m) => m.did)];
} else {
const wanted = new Set(scope.countries === 'all' ? [] : scope.countries);
- const seed = wanted.has('nl')
- ? memberDids
- : wanted.has('be')
- ? beMemberDids
- : wanted.has('hu')
- ? huMemberDids
- : [];
const matched = selfAsserted.filter((m) => m.countries.some((c) => wanted.has(c))).map((m) => m.did);
- didList = [...seed, ...matched];
+ didList = [...seedOf(scope), ...matched];
}
const dids = [...new Set(didList)].filter((d) => !blocked.has(d));
- const base = memberListUri ? await getListMembers(memberListUri) : await getProfiles(dids);
+ // A public Bluesky list (national scopes only), when set, replaces the seed/self-asserted profiles.
+ const listUri = scope.aggregate ? null : (scope.memberListUri ?? null);
+ const base = listUri ? await getListMembers(listUri) : await getProfiles(dids);
// Enrich each member with current position + location from their Sifa records.
// Bounded concurrency (not Promise.all) so we don't burst past Sifa's 60/min rate limit
@@ -68,7 +66,7 @@ async function loadCommunity(): Promise {
// The homepage feed is this scope's curated custom feed generator (see data/feed.ts): members
// plus the wider atproto conversation for that country. The .eu aggregate has none, so no fetch.
- const homeFeed = feedForScope(scope.cc);
+ const homeFeed = feedForScope(scope);
const feed = homeFeed ? await getFeed(homeFeed.uri, 20) : [];
// Arbitrary /members order so no member is permanently first. Seeded from the set of member DIDs