Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664// Per-domain build configuration.//// One codebase, three real domains, selected at build time with SITE_SCOPE=nl|be|eu. Each domain// is a complete standalone site and there are NO redirects between them: all three are borrowed,// so redirecting two into the third would make one borrowed domain a single point of failure for// three national communities.//// The differences between builds are ordinary Astro mechanisms, not special cases:// 1. extra pages - getStaticPaths() returns [] when the scope does not want a route// 2. composition - a shared page renders an extra block when `aggregate` is true// 3. data selection - `countries` picks rows; scope: eu | global items appear everywhere//// This file is imported by astro.config.mjs, so it must stay dependency-free and side-effect-free.
import type { AccentPack } from './accents';
export type ScopeId = 'nl' | 'be' | 'no' | 'it' | 'ch' | 'hu' | 'pl' | 'es' | 'pt' | 'cz' | 'barcelona' | 'eu';
// Each country has a TWO-colour lever: `accent` (primary) + `accent2` (secondary), a flag-derived// //atproto.<cc> lockup. The packs (name -> hex, light + dark) live in ./accents.ts, the single// source of truth from which every colour asset (CSS, favicons, avatars, OG cards, map) is generated.export type { AccentPack };
/** A community verifier this scope delegates verification to. A scope can have several (Spain is the * first with two, covering different groups). Rendered on the /verify page so people pick the right * one. `did` is the identity key; `handle` + `url` are display/link; `remit` is a short localized * description of WHO this verifier covers (lang -> text, always include 'en'). */export interface Verifier { did: string; handle: string; /** Where to send people to get verified by this account (its site or profile). */ url: string; /** Locale -> one-line description of what this verifier covers. */ remit: Record<string, string>;}
/** 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; /** Countries whose items this build shows. 'all' = the aggregate (.eu). */ countries: readonly string[] | 'all'; /** Locales, in order; the first is the default. Astro resolves i18n at build time, which is * the main reason three builds beat one multi-domain deploy. */ locales: readonly string[]; /** True only for .eu: adds per-country breakdowns and the /stats directory. */ aggregate: boolean; /** Which tier of community this scope is. 'country' (default) is a national site; 'city' is a * single-city community (e.g. atproto.barcelona). A city is NEVER a country and must not stand in * for one (hard rule): city scopes are excluded from the .eu "Country communities" network strip, * and their members are a curated roster (managers + memberSeed + their own signed-in members), * NOT everyone in the surrounding country. The city still sits inside a country for context * (`countries`/REGION), so its map, estimate and events use that country. */ tier?: 'country' | 'city'; /** Whether this build has a /verify page, homepage verify teaser and footer link. Only .nl * self-verifies; .be delegates to belgium-atmosphe.re (see VerifyPage). A scope that is not a * verifier (e.g. .hu) sets this false, and the page route + teaser + footer link drop out * entirely rather than rendering a self-verifier page it has no authority to back. */ verify: boolean; /** Languages whose speakers are shown as a SEPARATE, clearly-labelled section on /builders. * Affinity for discovery ("you could work with this person"), NEVER evidence of membership. * Language does not confirm a country; keep these out of the country list. */ relatedLangs: readonly string[]; /** Countries a join from this build asserts (ISO 3166-1 alpha-2, lowercase). * Empty means the build must NOT auto-join: .eu is an aggregate view with no country of its * own, so it asks instead of guessing. */ joinCountries: readonly string[]; // --- design system (multi-site design language handoff, 2026-09-12) --- /** ISO 3166-1 alpha-2 lowercase, or 'eu' for the aggregate. Drives the wordmark (//atproto.<cc>), * the map highlight, and favicon/OG filenames. */ cc: string; /** Primary colour: drives all UI + the wordmark's first slash and dot. Sets data-accent on <html>. */ accent: AccentPack; /** Secondary colour: the wordmark's SECOND slash only (flag-derived two-colour lockup). Sets * data-accent2 on <html>. Equal to `accent` for single-colour countries (e.g. .nl orange/orange). */ accent2: AccentPack; /** Cities to plot on the localness map as [lon, lat]. Member counts come from data at build. */ cities: Record<string, readonly [number, number]>; /** Optional geographic frame for the localness map as [west, south, east, north] (lon/lat). A * CITY scope uses it to zoom the map to its REGION (e.g. Barcelona → Catalunya) while the home * country (statsCc) still colours the land. National scopes omit it and auto-fit their country. */ mapBox?: readonly [number, number, number, number]; /** Locale -> endonym for the header switcher (shown alphabetically, never a code, never a flag). */ endonyms: Record<string, string>;
// --- 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<NavToggles>; /** When verification is DELEGATED, the verifier account(s) this scope points people to (Belgium * has one; Spain has two, covering different groups). WHAT IT DOES: renders a "Community * verifiers" section on /verify, each with its remit + a link. Only meaningful with * `verify: true`. DEFAULT: [] (the scope verifies itself, like .nl, or with `verify: false` * nobody does and there is no /verify page). */ verifiers?: readonly Verifier[]; /** 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 * and never treat the *.atproto.<cc> handle as identity: a national build must point at its OWN * account, never .nl's. .be delegates to its verifier (belgium-atmosphe.re); every other scope * is its own @atproto.<cc>. .ch's handle is not DNS-resolvable yet, which is exactly why the URL * is built from the DID (always resolves) rather than the handle. */ social: { did: string; handle: string };}
// Spain's two community verifiers, shared by the .es country scope AND the Barcelona city scope// (both delegate to the same pair). They cover different groups: KriptiES verifies Spanish-speaking// accounts worldwide; spain-atmosphe.re focuses on accounts based in Spain. Remit localized en/es/ca.const SPANISH_VERIFIERS: readonly Verifier[] = [ { did: 'did:plc:ivfnn6frjjmojlbltgc2nu25', handle: 'kriptiesfundacion.org', url: 'https://mu.social/profile/kriptiesfundacion.org', remit: { en: 'Verifies Spanish-speaking accounts worldwide, not only those based in Spain.', es: 'Verifica cuentas en español de todo el mundo, no solo las radicadas en España.', ca: 'Verifica comptes en castellà d\'arreu del món, no només els radicats a Espanya.', }, }, { did: 'did:plc:mggfiuvw3iz6dkxafrvsyfbx', handle: 'spain-atmosphe.re', url: 'https://spain-atmosphe.re/', remit: { en: 'Verifies accounts based in Spain.', es: 'Verifica cuentas radicadas en España.', ca: 'Verifica comptes radicats a Espanya.', }, },];
export const SCOPES: Record<ScopeId, Scope> = { nl: { site: 'https://atproto.nl', countries: ['nl'], locales: ['en', 'nl'], aggregate: false, verify: true, relatedLangs: [], joinCountries: ['nl'], cc: 'nl', // Dutch national orange, single-colour (both slashes) — the family anchor. accent: 'orange', accent2: 'orange', 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], Rotterdam: [4.48, 51.92], Eindhoven: [5.47, 51.44], Groningen: [6.57, 53.22], }, endonyms: { en: 'English', nl: 'Nederlands' }, social: { did: 'did:plc:rldh4m4qtswi7tq4rp23r27q', handle: 'atproto.nl' }, }, be: { site: 'https://atproto.be', countries: ['be'], locales: ['en', 'nl', 'fr'], aggregate: false, verify: true, relatedLangs: ['nl', 'fr'], joinCountries: ['be'], cc: 'be', // Belgian flag black-yellow-red: yellow + red (black is invisible on both themes). accent: 'yellow', accent2: 'red', // .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 ], verifiers: [ { did: 'did:plc:zsf5p7rqilz2qvyd7ezmxrfj', handle: 'belgium-atmosphe.re', url: 'https://belgium-atmosphe.re/#verify', remit: { en: 'Verifies accounts that belong to the Belgian AT Protocol community.', nl: 'Verifieert accounts die tot de Belgische AT Protocol-community behoren.', fr: 'Vérifie les comptes qui font partie de la communauté AT Protocol belge.', }, }, ], 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], Antwerpen: [4.4, 51.22], Leuven: [4.7, 50.88], }, endonyms: { en: 'English', fr: 'Français', nl: 'Nederlands' }, // No @atproto.be account: .be delegates its community presence to belgium-atmosphe.re (also its // verifier), so its social links point there rather than at a non-existent local account. social: { did: 'did:plc:zsf5p7rqilz2qvyd7ezmxrfj', handle: 'belgium-atmosphe.re' }, }, no: { site: 'https://atproto.no', countries: ['no'], // EN + Norwegian Bokmål (nb). The nb strings are machine-authored (Kimi), pending review by a // local Norwegian maintainer — the design pack ships ['en','nb']. locales: ['en', 'nb'], aggregate: false, // Not a verifier: .no does not self-verify, so no /verify page, teaser or footer link. Only // .nl (trusted verifier) and .be (delegated to belgium-atmosphe.re) set this true. verify: false, relatedLangs: [], joinCountries: ['no'], cc: 'no', // Norwegian flag red + blue (the two colours of the field and cross; white omitted). accent: 'red', accent2: 'cobalt', managers: ['did:plc:ad5bwszsc5m5jpj2sfa6uzjk'], // erlend.sh feed: { rkey: 'atproto-no' }, cities: { Oslo: [10.75, 59.91], Bergen: [5.32, 60.39], Trondheim: [10.4, 63.43], Stavanger: [5.73, 58.97], Tromsø: [18.96, 69.65], }, endonyms: { en: 'English', nb: 'Norsk bokmål' }, social: { did: 'did:plc:ps7d7srdap5nwshvoqjqi7l2', handle: 'atproto.no' }, }, hu: { site: 'https://atproto.hu', countries: ['hu'], locales: ['en', 'hu'], aggregate: false, verify: false, relatedLangs: [], joinCountries: ['hu'], cc: 'hu', // Hungarian flag red-white-green: green + red. accent: 'green', accent2: 'red', managers: ['did:plc:me4kozmcrc74z2wgkvqvkbfd'], // joeinn.es cities: { Budapest: [19.04, 47.50], Debrecen: [21.63, 47.53], Szeged: [20.15, 46.25], Miskolc: [20.78, 48.10], Pécs: [18.23, 46.08], Győr: [17.64, 47.68], }, endonyms: { en: 'English', hu: 'Magyar' }, social: { did: 'did:plc:xqlsfwqdhq4ts3lkmrlnnubj', handle: 'atproto.hu' }, }, it: { site: 'https://atproto.it', countries: ['it'], // EN + Italian. The `it` strings are machine-authored, pending review by the local // maintainer (@matteomarjanovic.com). locales: ['en', 'it'], aggregate: false, // Not a verifier (no /verify page, teaser or footer link) — same as .no/.hu. verify: false, relatedLangs: [], joinCountries: ['it'], cc: 'it', // Italian flag green-white-red: aqua (a cooler green, distinct from .hu's lime green) + red. accent: 'aqua', accent2: 'red', managers: ['did:plc:f7xezgbbwhlb66uszekr5ksc'], // matteomarjanovic.com cities: { Roma: [12.5, 41.9], Milano: [9.19, 45.46], Torino: [7.69, 45.07], Napoli: [14.27, 40.85], Firenze: [11.26, 43.77], Bologna: [11.34, 44.49], }, endonyms: { en: 'English', it: 'Italiano' }, social: { did: 'did:plc:3w3uxg3nt4xob6kfv4exm2li', handle: 'atproto.it' }, }, ch: { site: 'https://atproto.ch', countries: ['ch'], // EN + German (most widely spoken language in Switzerland). The `de` strings are // machine-authored and have no local maintainer yet to review them. FR/IT can be added // later where cheap. locales: ['en', 'de'], aggregate: false, verify: false, relatedLangs: [], joinCountries: ['ch'], cc: 'ch', // Swiss flag red + white: red + grey (grey stands in for white, invisible on cream). accent: 'red', accent2: 'grey', cities: { Zürich: [8.55, 47.37], Genève: [6.14, 46.2], Basel: [7.59, 47.56], Bern: [7.45, 46.95], Lausanne: [6.63, 46.52], }, endonyms: { en: 'English', de: 'Deutsch' }, // @atproto.ch exists but its handle is not DNS-resolvable yet, so key on the DID (the URL is // built from it, which resolves regardless of the handle's DNS state). social: { did: 'did:plc:iry7dmntlflldbhnf7nznr2s', handle: 'atproto.ch' }, }, pl: { site: 'https://atproto.pl', countries: ['pl'], // EN + Polish. The `pl` strings are machine-authored, pending review by a local Polish // maintainer (none yet) — the design pack ships ['en','pl']. locales: ['en', 'pl'], aggregate: false, // Not a verifier (no /verify page, teaser or footer link) — same as .no/.hu/.it/.ch. verify: false, relatedLangs: [], joinCountries: ['pl'], cc: 'pl', // Polish flag white-over-red: white -> grey (invisible on cream) + red. The reverse of .ch // (red/grey), so the grey/red pair stays distinct. UI leads with grey, not red (UI-accent rule). accent: 'grey', accent2: 'red', cities: { Warszawa: [21.01, 52.23], Kraków: [19.94, 50.06], Wrocław: [17.04, 51.11], Poznań: [16.93, 52.41], Gdańsk: [18.65, 54.35], Łódź: [19.46, 51.76], }, endonyms: { en: 'English', pl: 'Polski' }, social: { did: 'did:plc:524l67hpuzkx2fn573rgxpqk', handle: 'atproto.pl' }, }, es: { site: 'https://atproto.es', countries: ['es'], // Spanish + EN, Spanish default (the root `/` serves `es`, English lives at `/en`). Spain is the // first scope to lead with its own language rather than English. The `es` strings are // machine-authored, pending review by a local Spanish maintainer (none yet). locales: ['es', 'en'], aggregate: false, // A verifier scope, but it does NOT self-verify: verification is delegated to TWO community // verifiers with different remits (see `verifiers` below). `verify: true` turns on the /verify // page, teaser and footer link; the page lists both verifiers rather than a self-verify flow. verify: true, relatedLangs: [], joinCountries: ['es'], cc: 'es', // Spanish flag red-yellow-red (rojigualda): crimson (a deeper red, distinct from .de red/yellow // and .be yellow/red) + yellow/gold. crimson reads as red, so the UI leads with yellow, not red // (UI-accent rule); the wordmark keeps crimson first. accent: 'crimson', accent2: 'yellow', managers: [ 'did:plc:ditvixxmddbyb7ctyz2pp4an', // juanesteban.bsky.social 'did:plc:45ullteprjewg7pvyy2bojc3', // marcarmengou.com 'did:plc:dhckga4os5p3owgewd4hevca', // didiermortier.eu ], // Spain is the first scope with TWO verifiers (shared with the Barcelona city scope). verifiers: SPANISH_VERIFIERS, feed: { rkey: 'atproto-es' }, // gui.do/feed/atproto-es cities: { Madrid: [-3.70, 40.42], Barcelona: [2.17, 41.39], Valencia: [-0.38, 39.47], Sevilla: [-5.99, 37.39], Bilbao: [-2.93, 43.26], Zaragoza: [-0.89, 41.65], }, endonyms: { en: 'English', es: 'Español' }, social: { did: 'did:plc:jsdsxwyjoh5fc3amfraroe3s', handle: 'atproto.es' }, }, pt: { site: 'https://atproto.pt', countries: ['pt'], // EN + Portuguese. The `pt` strings are machine-authored, pending review by a local Portuguese // maintainer (none yet) — the design pack ships ['en','pt']. locales: ['en', 'pt'], aggregate: false, // Not a verifier (no /verify page, teaser or footer link) — same as .no/.hu/.it/.ch/.pl/.es. verify: false, relatedLangs: [], joinCountries: ['pt'], // Local site managers (shown as avatars), keyed on DID per the hard rule. managers: [ 'did:plc:ditvixxmddbyb7ctyz2pp4an', // juanesteban.bsky.social 'did:plc:6cbrxnrfmwpb7rhjkihqezzh', // caesar.dev ], cc: 'pt', // Portuguese flag green-red (verde-escuro + vermelho): pine (a deep evergreen, distinct from // .hu/.it greens) + red. pine is not red, so the UI leads with pine; the wordmark keeps pine first. accent: 'pine', accent2: 'red', cities: { Lisboa: [-9.14, 38.72], Porto: [-8.61, 41.15], Braga: [-8.43, 41.55], Coimbra: [-8.43, 40.21], Faro: [-7.93, 37.02], Funchal: [-16.92, 32.65], }, endonyms: { en: 'English', pt: 'Português' }, // atproto.pt resolves to this DID (account atprotopt.eurosky.social); keyed on the DID per the // hard rule, handle is display only. social: { did: 'did:plc:6xghd5omvsliv6cslozvgrlz', handle: 'atproto.pt' }, }, cz: { site: 'https://atproto.cz', countries: ['cz'], // Czech + EN, Czech default (the root `/` serves `cs`, English lives at `/en`). The `cs` // strings are NATIVE, contributed by the local maintainer @tomaslatal.cz (Tangled PR #5), so // unlike the machine-authored packs they ship reviewed. locales: ['cs', 'en'], aggregate: false, verify: false, relatedLangs: [], joinCountries: ['cz'], cc: 'cz', // Czech flag white-red + blue triangle: grey (stands in for white, invisible on cream) + cobalt // blue. grey leads the UI (not red); the pair is distinct from .pl grey/red and .no red/cobalt. accent: 'grey', accent2: 'cobalt', managers: ['did:plc:xolhw4alvk3vihixjxs4wazp'], // tomaslatal.cz cities: { Praha: [14.42, 50.09], Brno: [16.61, 49.2], Ostrava: [18.29, 49.84], Plzeň: [13.38, 49.75], Olomouc: [17.25, 49.59], Liberec: [15.06, 50.77], }, endonyms: { en: 'English', cs: 'Čeština' }, social: { did: 'did:plc:tif2jfivlmjqnjwjep6ivndh', handle: 'atproto.cz' }, }, barcelona: { // The first CITY scope (tier: 'city'), not a country. A curated Barcelona community: its member // list is managers + memberSeed + its own signed-in members, never all of Spain. It sits inside // Spain for context (map/estimate/events use `countries: ['es']`), but is excluded from the .eu // "Country communities" strip and the footer's national list. See // decisions/2026-09-21-centralized-country-config.md + the city tier notes. tier: 'city', site: 'https://atproto.barcelona', countries: ['es'], // Catalan first, then Spanish, then English — Barcelona/Catalan audience. `ca` strings are // machine-authored, pending native review; the root `/` serves Catalan, `/es` + `/en` follow. locales: ['ca', 'es', 'en'], aggregate: false, // Delegates verification to the same two verifiers as .es (Barcelona is in Spain), so its // /verify page lists both. verify:true turns the page/teaser/footer link on. verify: true, verifiers: SPANISH_VERIFIERS, relatedLangs: [], // A join here asserts Spanish membership (there is no city-level membership record), so joiners // appear on atproto.es; Barcelona lists whom it wants via managers/memberSeed. joinCountries: ['es'], cc: 'barcelona', // Barcelona / Senyera: four red bars on gold + the St George cross. Gold (yellow) + a deep // Senyera red (crimson). yellow leads (not red) per the UI-accent rule; the pair is distinct // from .es crimson/yellow, .be yellow/red and .de red/yellow. accent: 'yellow', accent2: 'crimson', managers: ['did:plc:dhckga4os5p3owgewd4hevca'], // didiermortier.eu // Hide the Stats item: the estimate/stats page models a COUNTRY's atproto population, which is // misleading on a single-city site. Everything else (members, events, tools, builders) shows. nav: { stats: false }, social: { did: 'did:plc:ktf7tvvzhi5nszz2xtlerbhr', handle: 'atproto.barcelona' }, cities: { Barcelona: [2.17, 41.39], }, // Frame the map on Catalunya (Barcelona's region), not all of Spain; Spain still colours the land. mapBox: [0.1, 40.4, 3.4, 42.95], endonyms: { ca: 'Català', es: 'Español', en: 'English' }, }, eu: { site: 'https://atproto.eu', countries: 'all', locales: ['en'], aggregate: true, // The aggregate is not a verifier: it has no country of its own to vouch for, and each national // scope handles (or delegates) its own verification. So no /verify page, teaser or footer link. verify: false, relatedLangs: [], joinCountries: [], cc: 'eu', // EU flag: blue field + gold stars. Teal-blue + gold. accent: 'blue', accent2: 'yellow', 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' }, },};
export const DEFAULT_SCOPE: ScopeId = 'nl';
/** The site's own name, e.g. "atproto.be". * * Derived from the scope rather than the string catalogue: it is the same in every language, and * hardcoding it per locale meant the .be build rendered "atproto.nl" in its French <title>. */export function siteName(scope: Scope): string { return scope.site.replace(/^https?:\/\//, '');}
/** Packs that read as red. Red is the web's error/warning colour, so a red primary makes the UI * (big numbers, links, buttons) look like an alert. We never LEAD the site UI with these — the * wordmark still uses them (logo colours are fixed by the flag), only the UI accent avoids red. */export const RED_PACKS = new Set<AccentPack>(['red', 'crimson']);
/** Which pack drives the site UI accent (data-ui on <html>): the scope's primary, unless that * primary is red, in which case the non-red secondary leads. This keeps the wordmark's flag-derived * colour order intact (slash 1 stays the true primary via --wm-accent) while the UI reads calm. * If both are red (none today) we keep the primary rather than invent a colour. */export function uiAccentPack(scope: Scope): AccentPack { return RED_PACKS.has(scope.accent) && !RED_PACKS.has(scope.accent2) ? scope.accent2 : scope.accent;}
/** The scope's own account profile on the network (mu.social), built from the DID so it resolves * even when the handle's DNS is not set up (e.g. .ch). This is the single place the mu.social host * and the /profile/<did> shape live — every consumer (Say hi, the footer Follow button and * mu.social link, the privacy "reach us" link, the Organization JSON-LD) goes through it. */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 ?? {}) };}
/** The country a scope's population stats/estimate are ABOUT. For a national scope it is its own cc; * for a CITY scope (e.g. atproto.barcelona) we have no city-level numbers, so it is the surrounding * country — the estimate and /community-size then read as "in Spain", not "in Barcelona". */export function statsCc(scope: Scope): string { return scope.tier === 'city' && scope.countries !== 'all' ? (scope.countries[0] ?? scope.cc) : scope.cc;}
export function isScopeId(v: unknown): v is ScopeId { return typeof v === 'string' && v in SCOPES;}
/** Resolve a scope id from a raw value (usually process.env.SITE_SCOPE). * * Throws on an unknown non-empty value rather than falling back. A typo silently building the * Dutch site and deploying it to atproto.be would be a much worse failure than a red build. */export function resolveScopeId(raw?: string | null): ScopeId { if (raw == null || raw === '') return DEFAULT_SCOPE; const v = raw.trim().toLowerCase(); if (!isScopeId(v)) { throw new Error( `Unknown SITE_SCOPE "${raw}". Expected one of: ${Object.keys(SCOPES).join(', ')}.`, ); } return v;}
export function getScope(raw?: string | null): Scope { return SCOPES[resolveScopeId(raw)];}
/** Does this build show an item tagged with these countries and scope? * * `scope: 'eu' | 'global'` items appear on every build by rule, which is what keeps a quiet * national site from ever looking empty. Never put 'eu' inside `countries` -- that is what the * scope field is for, and every filter would then need a special case. */export function showsItem( scope: Scope, item: { countries?: readonly string[]; scope?: string },): boolean { if (item.scope === 'eu' || item.scope === 'global') return true; if (scope.countries === 'all') return true; const wanted = new Set(scope.countries); return (item.countries ?? []).some((c) => wanted.has(c));}