From c9447f514b548f76230c371e06a0d381618324eb Mon Sep 17 00:00:00 2001 From: Guido Jansen Date: Mon, 21 Sep 2026 14:36:05 +0200 Subject: [PATCH] refactor(config): centralize per-country settings into scopes.ts control panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make scopes.ts the single documented control panel a local community manager edits, folding in settings that were scattered across data modules. Every new field is optional with a documented default, so a new country only writes what differs. See decisions/2026-09-21-centralized-country-config.md. New per-scope fields (all optional, documented inline): - managers: string[] — manager DIDs; shown as managers AND folded into members. Replaces the single `maintainer` handle (AboutPage's "no maintainer" state now keys on managers.length). - memberSeed: string[] — the manual member seed, moved from members.ts (memberDids/beMemberDids/huMemberDids). - memberListUri — optional public Bluesky list seed (moved from members.ts). - eventDids: {did,handle}[] — EXTRA atmo.rsvp sources; each scope's own social.did is now ALWAYS an event source automatically (previously only the hand-listed EVENTS_ACCOUNTS were fetched, so es/pt/be/etc. never showed their own events). - feed: {rkey} — homepage feed, moved from feed.ts. - nav: per-item header show/hide (members/stats/events/tools/builders), default all shown, via navFor(); Base.astro gates the optional links. - verifierDid — makes Belgium's delegated verifier explicit. Consumers now read from the scope: home.ts builds the member union (managers + seed + signed-in, deduped; .eu aggregates all national scopes), feed.ts turns scope.feed.rkey into the URI, events-account.ts derives EVENTS_ACCOUNTS from the scopes plus orphan accounts (atproto.wien). members.ts keeps only the non-per-country bits. scopes.test.ts asserts the new fields are DIDs and nav resolves against defaults. Manager-facing guide at src/config/README.md. No behaviour change to existing sites beyond the intended one: each country now surfaces atmo.rsvp events posted on its own account, and managers appear in the member list. --- src/components/pages/AboutPage.astro | 2 +- src/components/pages/HomePage.astro | 2 +- src/config/README.md | 44 +++++++++ src/config/scopes.test.ts | 34 ++++++- src/config/scopes.ts | 131 ++++++++++++++++++++++++--- src/data/events-account.ts | 23 ++++- src/data/feed.ts | 35 ++++--- src/data/members.ts | 71 ++------------- src/layouts/Base.astro | 15 +-- src/lib/atproto/home.ts | 30 +++--- 10 files changed, 267 insertions(+), 120 deletions(-) create mode 100644 src/config/README.md diff --git a/src/components/pages/AboutPage.astro b/src/components/pages/AboutPage.astro index be5f063..cfd571b 100644 --- a/src/components/pages/AboutPage.astro +++ b/src/components/pages/AboutPage.astro @@ -12,7 +12,7 @@ const scopeId = resolveScopeId(import.meta.env.PUBLIC_SITE_SCOPE); const isNl = scopeId === 'nl'; // No local maintainer yet (e.g. .be) -> the caretaker statement: honest about who runs it and that // it's meant to be handed over. The plan calls this the highest-value non-technical signal. -const noMaintainer = getScope(scopeId).maintainer === null; +const noMaintainer = (getScope(scopeId).managers ?? []).length === 0; --- diff --git a/src/components/pages/HomePage.astro b/src/components/pages/HomePage.astro index fdda223..364c950 100644 --- a/src/components/pages/HomePage.astro +++ b/src/components/pages/HomePage.astro @@ -96,7 +96,7 @@ if (scopeId === 'nl') { const showEvents = scopeId === 'nl' || scope.aggregate; // Each national scope has its own custom feed (Dutch/Belgian/Norwegian Atmosphere); the .eu // aggregate has none. Show the feed section only where a feed exists, and point it at that feed. -const homeFeed = feedForScope(scope.cc); +const homeFeed = feedForScope(scope); const showFeed = !!homeFeed; const calEvents = showEvents ? await getCalendarEvents() : []; const upcomingRows: EventRow[] = showEvents diff --git a/src/config/README.md b/src/config/README.md new file mode 100644 index 0000000..a05e73e --- /dev/null +++ b/src/config/README.md @@ -0,0 +1,44 @@ +# Country config — the community-manager control panel + +Everything a local community manager tunes for their country lives in **one file: `scopes.ts`**. +Each country is one entry in `SCOPES` (keyed by its two-letter code: `nl`, `be`, `pt`, …). Change a +value, open a pull request, and the next deploy rebuilds that country's site. + +Every field below is documented inline in `scopes.ts` too (hover the field). This README is the +friendly overview. + +## The knobs + +| Field | What it does | Default if you omit it | +|---|---|---| +| `social` | Your community's account: `{ did, handle }`. The DID is the real identity (survives handle renames); the handle is display only. Drives "Say hi", the footer follow, **and** is automatically an events source. | required | +| `locales` | The languages your site is built in, in order. **The first is the default** shown at `/`; others live under `/` (e.g. `['es','en']` → Spanish at `/`, English at `/en`). | required | +| `verify` | `true` shows the verifier page, homepage teaser and footer link. Set `true` only if you actually verify community members. | required | +| `verifierDid` | If verification is **delegated** to another account (e.g. Belgium → belgium-atmosphe.re), its DID. Only meaningful with `verify: true`. | `null` (you verify yourself, or nobody does) | +| `managers` | The people who run this site, as **DIDs**. Shown as the site's managers **and** listed as members (deduped). Empty = the "help run this" invitation shows. | `[]` | +| `memberSeed` | A manual starter list of members, as **DIDs** — people you vouch are part of the community before anyone signs in. Deduped with managers and everyone who signs in. | `[]` (the list still fills from sign-ins) | +| `memberListUri` | A public Bluesky list to use as the member roster instead of the seed. | `null` | +| `eventDids` | **Extra** atmo.rsvp accounts to pull events from, as `{ did, handle }` — beyond your own `social.did`, which is **always** included automatically. Use for sibling city chapters. | `[]` | +| `feed` | Your homepage feed: `{ rkey }` of the custom feed generator (under gui.do's account). | `null` (no homepage feed) | +| `nav` | Hide optional menu items: `{ members, stats, events, tools, builders }`, each `true`/`false`. `About`/home always show; `Verify` follows `verify`. | all shown | +| `accent` / `accent2` | Your flag's two wordmark colours (pack names from `accents.ts`). | required | +| `cities` | Cities to plot on the map, `Name: [lon, lat]`. | required | +| `endonyms` | Each locale's name in its own language, for the switcher. | required | + +## Common tasks + +- **Add a manager** → add their DID to `managers`. (Resolve a handle to a DID at + `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=`.) +- **Show your own events** → nothing to do: any atmo.rsvp event on your `social.did` appears + automatically. To also pull a city chapter, add `{ did, handle }` to `eventDids`. +- **Change the default language** → put that locale first in `locales`. +- **Hide an empty section** (e.g. no tools yet) → `nav: { tools: false }`. +- **Turn on the homepage feed** → set `feed: { rkey: 'atproto-' }` once the feed is published. + +## Rules that keep us safe + +- **Always DIDs, never handles** for identity (`managers`, `memberSeed`, `eventDids[].did`, + `verifierDid`, `social.did`). Handles can be re-registered; DIDs can't. The tests enforce this. +- Don't point anything at a `*.atproto.` handle as an identity key — those domains are borrowed. + +Background and rationale: `decisions/2026-09-21-centralized-country-config.md` in the workspace. diff --git a/src/config/scopes.test.ts b/src/config/scopes.test.ts index ff3a53b..90e7aec 100644 --- a/src/config/scopes.test.ts +++ b/src/config/scopes.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { SCOPES } from './scopes'; +import { SCOPES, NAV_DEFAULTS, navFor } from './scopes'; import { ACCENT_PACKS } from './accents'; const scopes = Object.entries(SCOPES); +const isDid = (s: string) => /^did:(plc|web):/.test(s); describe('scope accent packs', () => { it('every scope references packs that exist in accents.ts', () => { @@ -34,3 +35,34 @@ describe('scope accent packs', () => { expect(new Set(ccs).size).toBe(ccs.length); }); }); + +// The community-manager control panel (decisions/2026-09-21-centralized-country-config.md). These +// guard the identity-sensitive fields: managers/seeds/verifier/events are DIDs (never handles, per +// the hard rule), and nav toggles resolve against the documented defaults. +describe('scope control-panel fields', () => { + it('managers, memberSeed, eventDids and verifierDid are DIDs, never handles', () => { + for (const [id, s] of scopes) { + for (const did of s.managers ?? []) + expect(isDid(did), `${id} manager "${did}" must be a DID`).toBe(true); + for (const did of s.memberSeed ?? []) + expect(isDid(did), `${id} memberSeed "${did}" must be a DID`).toBe(true); + for (const e of s.eventDids ?? []) + expect(isDid(e.did), `${id} eventDids "${e.did}" must be a DID`).toBe(true); + if (s.verifierDid) expect(isDid(s.verifierDid), `${id} verifierDid`).toBe(true); + } + }); + + it('navFor merges overrides over the all-shown defaults', () => { + for (const [, s] of scopes) { + expect(navFor(s)).toEqual({ ...NAV_DEFAULTS, ...(s.nav ?? {}) }); + } + // A scope with no nav override shows everything. + expect(navFor(SCOPES.nl)).toEqual(NAV_DEFAULTS); + }); + + it('a delegated verifier (verifierDid set) is also a verifier scope', () => { + for (const [id, s] of scopes) { + if (s.verifierDid) expect(s.verify, `${id} sets verifierDid but verify:false`).toBe(true); + } + }); +}); diff --git a/src/config/scopes.ts b/src/config/scopes.ts index 91052a5..5ab4fce 100644 --- a/src/config/scopes.ts +++ b/src/config/scopes.ts @@ -21,6 +21,26 @@ export type ScopeId = 'nl' | 'be' | 'no' | 'it' | 'ch' | 'hu' | 'pl' | 'es' | 'p // source of truth from which every colour asset (CSS, favicons, avatars, OG cards, map) is generated. export type { AccentPack }; +/** The optional header nav items a country can individually hide. `about` and the home/logo link + * are structural (always shown); `verify` is governed by the `verify` flag, not by these. */ +export interface NavToggles { + members: boolean; + stats: boolean; + events: boolean; + tools: boolean; + builders: boolean; +} + +/** The nav items in header order, with their default (all shown). The single source of truth for + * both `navFor()` and any UI that iterates the toggles. */ +export const NAV_DEFAULTS: NavToggles = { + members: true, + stats: true, + events: true, + tools: true, + builders: true, +}; + export interface Scope { /** Canonical origin. Feeds Astro's `site`, canonical URLs and the sitemap. */ site: string; @@ -55,10 +75,45 @@ export interface Scope { accent2: AccentPack; /** Cities to plot on the localness map as [lon, lat]. Member counts come from data at build. */ cities: Record; - /** Local maintainer handle, or null -> renders the invitation / "no local maintainer" state. */ - maintainer: string | null; /** Locale -> endonym for the header switcher (shown alphabetically, never a code, never a flag). */ endonyms: Record; + + // --- community-manager control panel (see decisions/2026-09-21-centralized-country-config.md) --- + // Everything below is a per-country knob a local manager may set. All are OPTIONAL: omit to take + // the documented default, so a new country only writes what differs from the norm. + + /** The people who run this country's site, by **DID** (not handle — DIDs survive renames). + * WHAT IT DOES: they are shown as the site's managers AND folded into the member list/hero + * (deduped by DID with the seed and signed-in members). WHEN TO SET: list every co-manager. + * DEFAULT: [] — which renders the "no local maintainer / help run it" invitation. */ + managers?: readonly string[]; + /** The manual "first feed" of members, by DID — people we vouch are part of the community before + * anyone has signed in. WHAT IT DOES: seeds the hero visual + /members list; deduped by DID with + * managers and signed-in members. WHEN TO SET: to pre-populate a new/quiet site. DEFAULT: [] + * (the list still fills from people who sign in as members). */ + memberSeed?: readonly string[]; + /** Optional public Bluesky list URI used as an EXTRA member seed source (at://…/app.bsky.graph.list/…). + * WHEN TO SET: when the community keeps its roster as a Bluesky list. DEFAULT: null. */ + memberListUri?: string | null; + /** EXTRA atmo.rsvp accounts whose calendar events we auto-publish — BEYOND this scope's own + * `social.did`, which is ALWAYS an event source automatically. WHAT IT DOES: pulls every event + * these accounts post onto this country's site. Each needs its `handle` (used to build the + * atmo.rsvp link + source badge). WHEN TO SET: for sibling city chapters (e.g. .nl adds + * atproto.amsterdam). DEFAULT: [] (only the main account's events show). */ + eventDids?: readonly { did: string; handle: string }[]; + /** The homepage feed generator for this scope (rkey under gui.do's feeds account). WHAT IT DOES: + * renders the "From the … community" feed on the homepage. WHEN TO SET: once a per-country feed + * is published. DEFAULT: null (no homepage feed). */ + feed?: { rkey: string } | null; + /** Per-item header menu visibility. WHAT IT DOES: hides an optional nav item (and its page is + * still built, just unlinked). WHEN TO SET: to hide a section that is empty for this country + * (e.g. no tools yet). DEFAULT: every item shown. `about`/home are structural; `verify` is + * governed by `verify` above, not here. */ + nav?: Partial; + /** When verification is DELEGATED to another account, its DID (e.g. Belgium → belgium-atmosphe.re). + * WHAT IT DOES: names who actually verifies, for the verify page copy. Only meaningful alongside + * `verify: true`. DEFAULT: null (we verify ourselves, or — with `verify:false` — nobody does). */ + verifierDid?: string | null; /** The community's own account on the network, the target of "Say hi" / the footer Follow button / * the footer mu.social link. `did` is the stable identity key (used for the Bluesky follow and to * build the profile URL); `handle` is display only. Per the project hard rule we key on the DID @@ -82,7 +137,35 @@ export const SCOPES: Record = { // Dutch national orange, single-colour (both slashes) — the family anchor. accent: 'orange', accent2: 'orange', - maintainer: '@gui.do', + managers: ['did:plc:45uheisi25szrjvjurfpritx'], // gui.do + feed: { rkey: 'atproto-nl' }, + eventDids: [{ did: 'did:plc:bs2glhnim6sqt3f4ccllwxej', handle: 'atproto.amsterdam' }], // sibling city chapter + memberSeed: [ + 'did:plc:mdjhvva6vlrswsj26cftjttd', // laurenshof.online — Laurens + 'did:plc:r5bee2hexwjtmhkfmfu2tphx', // mokkenstorm.dev — niels + 'did:plc:7kpq3n7brenbgyp2gx36hl6x', // julien.rbrt.fr — Julien Robert + 'did:plc:aq7owa5y7ndc2hzjz37wy7ma', // tijs.org — Tijs Teulings + 'did:plc:uyjvm2kqevlb4pa6sf63476h', // nickthesick.com — Nick The Sick + 'did:plc:3hj57ir63lsdlk6jlggdrzop', // tommi.space — Tommi + 'did:plc:diblntoainoud2g6qk5bawks', // nicr.dev — Nicolò Ribaudo + 'did:plc:cg2p4jwqojdspsyxn2otzsbs', // sammy.bsky.social — Sam Liebeskind + 'did:plc:xsx3bphrwkgeo3qnfjhzmdra', // hacdias.com — Henrique Dias + 'did:plc:2hgmrwevidwsxundvejdeam5', // tylerjfisher.com — Tyler Fisher + 'did:plc:wcs7zqone3b6dpij7aecosmu', // sebastix.bsky.social — Sebastian Hagens + 'did:plc:b3ethzamzumrezs4y7pibj6b', // jdot.blacksky.team — JD + 'did:plc:cdqpaz46zwmw4ntplh5txoms', // lefelys.com — Emil B + 'did:plc:e6byrf6y55gpb6t6ywdnpqnh', // benjamin.network — Benjamin + 'did:plc:lg3tzsxugmbiyxsz53ib3com', // jvtilburg.eurosky.social — Joris van Tilburg + 'did:plc:lbjgxkpi67g5yya7trkeemla', // nimobeeren.com — Nimo + 'did:plc:qxaugrh7755sxvmxndcvcsgn', // michelbestaat.thereforeiam.eu — Michel Bestaat + 'did:plc:vlblikmsgpx2i7fvxinrsvzu', // moll.dev — Tom + 'did:plc:tzuuahtbhkr7t3yjc7r7kqbt', // thomasboom.eurosky.social — Thomas Boom + 'did:plc:e7ewxmxs34v7p5sqqs2c4gzc', // sander.eurosky.social — Sander van der Waal + 'did:plc:swm4jmp3oxbbn7dubzmtj7ei', // marleenstikker.eurosky.social — Marleen Stikker + 'did:plc:whydbshthutv6dtqdd3zlwf2', // matteogabriele.npmx.social — Matteo Gabriele + 'did:plc:rhmoyr3gfl2pnypmuwmefz3z', // monnik.org — Edwin Gardner + 'did:plc:5rms2ebhdngu24hgsu3s2hqd', // angrydutchman.houseofmadness.org — AngryDutchman + ], cities: { Amsterdam: [4.9, 52.37], Utrecht: [5.12, 52.09], @@ -105,7 +188,27 @@ export const SCOPES: Record = { // Belgian flag black-yellow-red: yellow + red (black is invisible on both themes). accent: 'yellow', accent2: 'red', - maintainer: null, + // .be delegates both its community presence and verification to belgium-atmosphe.re; its managers + // and seed are the belgium-atmosphe.re people (belgium-atmosphe.re/#who). + managers: [ + 'did:plc:v4zpi74gy7enfiwke7hmoxv5', // pixeline.be + 'did:plc:2zxlmj2dvub7smpul2lvwqfk', // mathewlowry.eurosky.social + 'did:plc:nmk5tohklsobiohz7dde3pgz', // vdbdavid.eurosky.social + ], + verifierDid: 'did:plc:zsf5p7rqilz2qvyd7ezmxrfj', // belgium-atmosphe.re is the delegated verifier + feed: { rkey: 'atproto-be' }, + memberSeed: [ + 'did:plc:v4zpi74gy7enfiwke7hmoxv5', // pixeline.be + 'did:plc:izttpdp3l6vss5crelt5kcux', // robin.berjon.com + 'did:plc:linrigsaay5zenhg756ca6tg', // matthieu.bsky.team + 'did:plc:xpsroaaldjiognzrdk63k454', // amirakrari.dev + 'did:plc:qvgds53otyohus5lfcfjssk2', // tokono.ma + 'did:plc:nmk5tohklsobiohz7dde3pgz', // vdbdavid.eurosky.social + 'did:plc:2zxlmj2dvub7smpul2lvwqfk', // mathewlowry.eurosky.social + 'did:plc:h7i2odhlkhywtmhlfnhzyxd7', // ricafabi.eurosky.social + 'did:plc:qay5t74gafoql7x4j3ahzau7', // logandupont.com + 'did:plc:e4pttehx46z75wnajd3cuzjm', // philosophy.eurosky.social + ], cities: { Brussel: [4.35, 50.85], Gent: [3.72, 51.05], @@ -133,7 +236,8 @@ export const SCOPES: Record = { // Norwegian flag red + blue (the two colours of the field and cross; white omitted). accent: 'red', accent2: 'cobalt', - maintainer: null, + managers: ['did:plc:ad5bwszsc5m5jpj2sfa6uzjk'], // erlend.sh + feed: { rkey: 'atproto-no' }, cities: { Oslo: [10.75, 59.91], Bergen: [5.32, 60.39], @@ -156,7 +260,7 @@ export const SCOPES: Record = { // Hungarian flag red-white-green: green + red. accent: 'green', accent2: 'red', - maintainer: '@joeinn.es', + managers: ['did:plc:me4kozmcrc74z2wgkvqvkbfd'], // joeinn.es cities: { Budapest: [19.04, 47.50], Debrecen: [21.63, 47.53], @@ -183,7 +287,7 @@ export const SCOPES: Record = { // Italian flag green-white-red: aqua (a cooler green, distinct from .hu's lime green) + red. accent: 'aqua', accent2: 'red', - maintainer: '@matteomarjanovic.com', + managers: ['did:plc:f7xezgbbwhlb66uszekr5ksc'], // matteomarjanovic.com cities: { Roma: [12.5, 41.9], Milano: [9.19, 45.46], @@ -210,7 +314,6 @@ export const SCOPES: Record = { // Swiss flag red + white: red + grey (grey stands in for white, invisible on cream). accent: 'red', accent2: 'grey', - maintainer: null, cities: { Zürich: [8.55, 47.37], Genève: [6.14, 46.2], @@ -239,7 +342,6 @@ export const SCOPES: Record = { // (red/grey), so the grey/red pair stays distinct. UI leads with grey, not red (UI-accent rule). accent: 'grey', accent2: 'red', - maintainer: null, cities: { Warszawa: [21.01, 52.23], Kraków: [19.94, 50.06], @@ -269,7 +371,6 @@ export const SCOPES: Record = { // (UI-accent rule); the wordmark keeps crimson first. accent: 'crimson', accent2: 'yellow', - maintainer: null, cities: { Madrid: [-3.70, 40.42], Barcelona: [2.17, 41.39], @@ -297,7 +398,6 @@ export const SCOPES: Record = { // .hu/.it greens) + red. pine is not red, so the UI leads with pine; the wordmark keeps pine first. accent: 'pine', accent2: 'red', - maintainer: null, cities: { Lisboa: [-9.14, 38.72], Porto: [-8.61, 41.15], @@ -325,7 +425,8 @@ export const SCOPES: Record = { // EU flag: blue field + gold stars. Teal-blue + gold. accent: 'blue', accent2: 'yellow', - maintainer: '@gui.do', + managers: ['did:plc:45uheisi25szrjvjurfpritx'], // gui.do — the aggregate's caretaker + feed: { rkey: 'atproto-eu' }, cities: {}, endonyms: { en: 'English' }, social: { did: 'did:plc:53cfw2ck7qj2d6z5z2jcmadq', handle: 'atproto.eu' }, @@ -363,6 +464,12 @@ export function socialUrl(scope: Scope): string { return `https://mu.social/profile/${scope.social.did}`; } +/** The resolved header nav visibility for a scope: the scope's overrides merged over NAV_DEFAULTS + * (all shown). Callers gate optional nav links on this. */ +export function navFor(scope: Scope): NavToggles { + return { ...NAV_DEFAULTS, ...(scope.nav ?? {}) }; +} + export function isScopeId(v: unknown): v is ScopeId { return typeof v === 'string' && v in SCOPES; } diff --git a/src/data/events-account.ts b/src/data/events-account.ts index c5c345f..e85ad6e 100644 --- a/src/data/events-account.ts +++ b/src/data/events-account.ts @@ -22,13 +22,30 @@ export interface EventsAccount { countries: readonly string[]; } -export const EVENTS_ACCOUNTS: EventsAccount[] = [ - { did: 'did:plc:rldh4m4qtswi7tq4rp23r27q', handle: 'atproto.nl', countries: ['nl'] }, - { did: 'did:plc:bs2glhnim6sqt3f4ccllwxej', handle: 'atproto.amsterdam', countries: ['nl'] }, +// Derived from the scopes (the community-manager control panel): every national scope's OWN account +// (`social.did`) is an event source for its countries automatically (post an atmo.rsvp event on +// your country's main account and it appears), plus each scope's extra `eventDids` (sibling +// chapters like atproto.amsterdam on .nl). See decisions/2026-09-21-centralized-country-config.md. +const scopeEventsAccounts: EventsAccount[] = Object.values(SCOPES) + .filter((s) => !s.aggregate && s.countries !== 'all') + .flatMap((s) => { + const countries = [...(s.countries as readonly string[])]; + return [ + { did: s.social.did, handle: s.social.handle, countries }, + ...(s.eventDids ?? []).map((e) => ({ did: e.did, handle: e.handle, countries })), + ]; + }); + +// Accounts that belong to NO build scope but whose events we still carry (they surface only on the +// .eu aggregate, whose `countries: 'all'` matches everything). Kept explicit because there is no +// scope to derive their handle/country from. +const orphanEventsAccounts: EventsAccount[] = [ // Vienna's atproto meetup. No .at site, so these show only on the .eu aggregate. { did: 'did:plc:vao7qd2l2hy3tpolrqrkk4cj', handle: 'atproto.wien', countries: ['at'] }, ]; +export const EVENTS_ACCOUNTS: EventsAccount[] = [...scopeEventsAccounts, ...orphanEventsAccounts]; + // Individual events we have chosen to carry, from accounts we do NOT auto-publish. // // Adding an account to EVENTS_ACCOUNTS above pulls in everything it ever posts, which is right for diff --git a/src/data/feed.ts b/src/data/feed.ts index ae5583f..1b62cd2 100644 --- a/src/data/feed.ts +++ b/src/data/feed.ts @@ -1,27 +1,24 @@ -// Per-scope homepage feed. Each national scope has its own attie.ai custom feed generator in -// gui.do's repo (Dutch/Belgian/Norwegian Atmosphere): community members plus the wider atproto -// conversation relevant to that country, off-topic noise filtered out. The .eu aggregate has its -// own pan-European feed (European Atmosphere). -const FEED_DID = 'did:plc:45uheisi25szrjvjurfpritx'; // gui.do (the feed generators live in this repo) +// Per-scope homepage feed. Each national scope with a `feed` in scopes.ts has its own custom feed +// generator in gui.do's account (community members plus the wider atproto conversation for that +// country, off-topic noise filtered out); the .eu aggregate has the pan-European feed. Which rkey +// belongs to which country is a per-country knob in src/config/scopes.ts (`feed.rkey`); this module +// only turns that rkey into the AppView URI + the mu.social subscribe deeplink. +import type { Scope } from '../config/scopes'; -const uri = (rkey: string) => `at://${FEED_DID}/app.bsky.feed.generator/${rkey}`; -// Subscribe deeplink on mu.social (same path shape as bsky.app: /profile//feed/). -const subscribe = (rkey: string) => `https://mu.social/profile/gui.do/feed/${rkey}`; +const FEED_DID = 'did:plc:45uheisi25szrjvjurfpritx'; // gui.do (the feed generators live in this account) export interface HomeFeed { uri: string; subscribe: string; } -// Keyed by scope cc. Add a scope here once its feed is published. -const FEEDS: Record = { - nl: { uri: uri('atproto-nl'), subscribe: subscribe('atproto-nl') }, - be: { uri: uri('atproto-be'), subscribe: subscribe('atproto-be') }, - no: { uri: uri('atproto-no'), subscribe: subscribe('atproto-no') }, - eu: { uri: uri('atproto-eu'), subscribe: subscribe('atproto-eu') }, -}; - -/** The homepage feed for a scope, or null when it has none. */ -export function feedForScope(cc: string): HomeFeed | null { - return FEEDS[cc] ?? null; +/** The homepage feed for a scope, or null when it has none (no `feed` set in scopes.ts). */ +export function feedForScope(scope: Scope): HomeFeed | null { + if (!scope.feed) return null; + const { rkey } = scope.feed; + return { + uri: `at://${FEED_DID}/app.bsky.feed.generator/${rkey}`, + // Subscribe deeplink on mu.social (same path shape as bsky.app: /profile//feed/). + subscribe: `https://mu.social/profile/gui.do/feed/${rkey}`, + }; } diff --git a/src/data/members.ts b/src/data/members.ts index 43e7417..77530f1 100644 --- a/src/data/members.ts +++ b/src/data/members.ts @@ -1,71 +1,20 @@ -// Community membership — seed source. +// Community membership: non-per-country pieces. // -// The ~closed-group members, stored by DID:PLC (the stable identity key). Handles and -// profiles are resolved live from the public AppView at build time, so a rename never -// breaks the list. This is source 1 of the eventual 3-source model (see the -// locations-and-login decision): seed list + Sifa-derived + self-asserted PDS records, -// deduped by DID. A public Bluesky list can replace/augment this via `memberListUri`. +// The per-country member SEED lists now live in `src/config/scopes.ts` (`memberSeed` + `managers` +// per scope): the single community-manager control panel, see +// decisions/2026-09-21-centralized-country-config.md. Members are still a dedup-by-DID union of +// three sources at build (managers + seed + self-asserted `eu.atcommons.member` holders enumerated +// through the backlink index; Sifa-derived is the eventual fourth), assembled in +// `src/lib/atproto/home.ts`. +// +// This module keeps only the pieces that are NOT per country. // // (Do NOT source membership from the private DM group — it's private + caps at ~50/100.) -export const memberListUri: string | null = null; - // The commons DID. Self-asserted membership records (`eu.atcommons.member`) point at this via // their `community` field, and the build enumerates holders through the backlink index. -// Defined in ./membership so the browser bundle can import it without the seed list below. +// Defined in ./membership so the browser bundle can import it without pulling anything else in. export { COMMUNITY_DID } from './membership'; // DIDs to drop even if they self-assert membership (abuse lever; empty for now). export const memberBlocklist: string[] = []; - -export const memberDids: string[] = [ - // gui.do removed from the seed on purpose: he appears via his own self-asserted member - // record, enumerated through the backlink index. Proof the record path works. - 'did:plc:mdjhvva6vlrswsj26cftjttd', // laurenshof.online — Laurens - 'did:plc:r5bee2hexwjtmhkfmfu2tphx', // mokkenstorm.dev — niels - 'did:plc:7kpq3n7brenbgyp2gx36hl6x', // julien.rbrt.fr — Julien Robert - 'did:plc:aq7owa5y7ndc2hzjz37wy7ma', // tijs.org — Tijs Teulings - 'did:plc:uyjvm2kqevlb4pa6sf63476h', // nickthesick.com — Nick The Sick - 'did:plc:3hj57ir63lsdlk6jlggdrzop', // tommi.space — Tommi - 'did:plc:diblntoainoud2g6qk5bawks', // nicr.dev — Nicolò Ribaudo - 'did:plc:cg2p4jwqojdspsyxn2otzsbs', // sammy.bsky.social — Sam Liebeskind - 'did:plc:xsx3bphrwkgeo3qnfjhzmdra', // hacdias.com — Henrique Dias - 'did:plc:2hgmrwevidwsxundvejdeam5', // tylerjfisher.com — Tyler Fisher - 'did:plc:wcs7zqone3b6dpij7aecosmu', // sebastix.bsky.social — Sebastian Hagens - 'did:plc:b3ethzamzumrezs4y7pibj6b', // jdot.blacksky.team — JD - 'did:plc:cdqpaz46zwmw4ntplh5txoms', // lefelys.com — Emil B - 'did:plc:e6byrf6y55gpb6t6ywdnpqnh', // benjamin.network — Benjamin - 'did:plc:lg3tzsxugmbiyxsz53ib3com', // jvtilburg.eurosky.social — Joris van Tilburg - 'did:plc:lbjgxkpi67g5yya7trkeemla', // nimobeeren.com — Nimo - 'did:plc:qxaugrh7755sxvmxndcvcsgn', // michelbestaat.thereforeiam.eu — Michel Bestaat - 'did:plc:vlblikmsgpx2i7fvxinrsvzu', // moll.dev — Tom - 'did:plc:tzuuahtbhkr7t3yjc7r7kqbt', // thomasboom.eurosky.social — Thomas Boom - 'did:plc:e7ewxmxs34v7p5sqqs2c4gzc', // sander.eurosky.social — Sander van der Waal - 'did:plc:swm4jmp3oxbbn7dubzmtj7ei', // marleenstikker.eurosky.social — Marleen Stikker - 'did:plc:whydbshthutv6dtqdd3zlwf2', // matteogabriele.npmx.social — Matteo Gabriele - 'did:plc:rhmoyr3gfl2pnypmuwmefz3z', // monnik.org — Edwin Gardner - 'did:plc:5rms2ebhdngu24hgsu3s2hqd', // angrydutchman.houseofmadness.org — AngryDutchman -]; - -// Belgian seed: the belgium-atmosphe.re people (from belgium-atmosphe.re/#who), used as the -// atproto.be seed the way memberDids is for .nl. atproto.be is developer-focused; belgium-atmosphe.re -// keeps the general-user + Eurosky-verification side, so we share their member list and link to them. -// Deduped with self-asserted .be members at build. -export const beMemberDids: string[] = [ - 'did:plc:v4zpi74gy7enfiwke7hmoxv5', // pixeline.be - 'did:plc:izttpdp3l6vss5crelt5kcux', // robin.berjon.com - 'did:plc:linrigsaay5zenhg756ca6tg', // matthieu.bsky.team - 'did:plc:xpsroaaldjiognzrdk63k454', // amirakrari.dev - 'did:plc:qvgds53otyohus5lfcfjssk2', // tokono.ma - 'did:plc:nmk5tohklsobiohz7dde3pgz', // vdbdavid.eurosky.social - 'did:plc:2zxlmj2dvub7smpul2lvwqfk', // mathewlowry.eurosky.social - 'did:plc:h7i2odhlkhywtmhlfnhzyxd7', // ricafabi.eurosky.social - 'did:plc:qay5t74gafoql7x4j3ahzau7', // logandupont.com - 'did:plc:e4pttehx46z75wnajd3cuzjm', // philosophy.eurosky.social -]; - -// Hungarian seed: the atproto.hu maintainer(s), used as the .hu seed the way memberDids is for .nl. -// Deduped with self-asserted .hu members at build. -export const huMemberDids: string[] = [ - 'did:plc:me4kozmcrc74z2wgkvqvkbfd', // joeinn.es — Joe (atproto.hu maintainer) -]; diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 84ffc6b..9993240 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -1,5 +1,5 @@ --- -import { getScope, siteName, resolveScopeId, socialUrl, uiAccentPack } from '../config/scopes'; +import { getScope, siteName, resolveScopeId, socialUrl, uiAccentPack, navFor } from '../config/scopes'; import { localeHref, localeAlternates, OG_LOCALE, DEFAULT_LOCALE, LOCALES } from '../i18n/paths'; import { existingCommunities } from '../data/communities'; import '@fontsource-variable/atkinson-hyperlegible-next'; @@ -94,6 +94,9 @@ const canonical = `${site}${path}`; // The site name comes from the scope, not the catalogue: it does not change per language, and // the catalogue's copy said "atproto.nl" on every build. const scope = getScope(import.meta.env.PUBLIC_SITE_SCOPE); +// Per-country header menu visibility (defaults all shown); `about` is structural, `verify` is +// governed separately by `scope.verify`. See scopes.ts `nav` / navFor(). +const nav = navFor(scope); const SITE_NAME = siteName(scope); const pageTitle = title ? `${title} · ${SITE_NAME}` : SITE_NAME; const description = pageDescription ?? t('site.tagline'); @@ -239,11 +242,11 @@ const orgLd = {