simple list of pds servers with open registration
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Version filter fixes, trust-tiered sorting, moove here links

- Strip v prefix from GitHub version tags to match PDS health output
- Only show enriched servers (with geo/version data)
- Tier servers: featured (contact email) > ToS/PP > rest
- Featured servers always shown regardless of outdated toggle
- Add pdsmoover "Moove here" links, golden for featured servers
- Bump enrichment batch to 50 for faster initial population

+52 -21
+12 -3
backend/database/queries.ts
··· 135 135 orderClause = "user_count DESC NULLS LAST"; 136 136 } 137 137 138 - let whereClause = "is_open = 1"; 138 + let whereClause = "is_open = 1 AND last_enriched IS NOT NULL"; 139 139 const args: (string | number)[] = []; 140 140 141 141 if (!showOutdated && latestVersion) { 142 - whereClause += " AND (version = ? OR version IS NULL)"; 142 + // Featured servers (contact email) always show regardless of version 143 + whereClause += " AND (version = ? OR contact_email IS NOT NULL)"; 143 144 args.push(latestVersion); 144 145 } 145 146 147 + // Tier 1: featured (contact email), Tier 2: has ToS or PP, Tier 3: rest 148 + const tierExpr = ` 149 + CASE 150 + WHEN contact_email IS NOT NULL THEN 0 151 + WHEN terms_of_service IS NOT NULL OR privacy_policy IS NOT NULL THEN 1 152 + ELSE 2 153 + END`; 154 + 146 155 const result = await sqlite.execute({ 147 156 sql: 148 - `SELECT * FROM ${SERVERS_TABLE} WHERE ${whereClause} ORDER BY ${orderClause}`, 157 + `SELECT * FROM ${SERVERS_TABLE} WHERE ${whereClause} ORDER BY ${tierExpr}, ${orderClause}`, 149 158 args, 150 159 }); 151 160
+39 -17
backend/routes/pages.ts
··· 171 171 } 172 172 .trust-icons { display: flex; gap: 0.25rem; } 173 173 .trust-icon { 174 - width: 16px; 175 - height: 16px; 176 174 display: inline-flex; 177 175 align-items: center; 178 176 justify-content: center; 177 + padding: 0.1rem 0.3rem; 179 178 font-size: 0.7rem; 180 179 border-radius: 2px; 181 - background: #f3f4f6; 180 + background: #dcfce7; 181 + color: var(--green); 182 + text-decoration: none; 183 + } 184 + @media (prefers-color-scheme: dark) { 185 + .trust-icon { background: #052e16; } 186 + } 187 + .move-btn { 188 + display: inline-block; 189 + padding: 0.2rem 0.5rem; 190 + font-size: 0.75rem; 191 + border: 1px solid var(--muted); 192 + border-radius: 3px; 182 193 color: var(--muted); 183 194 text-decoration: none; 184 - title: attr(title); 195 + white-space: nowrap; 185 196 } 186 - .trust-icon.active { background: #dcfce7; color: var(--green); } 197 + .move-btn:hover { background: var(--muted); color: #fff; } 198 + .move-btn.featured { 199 + border-color: #ca8a04; 200 + color: #ca8a04; 201 + background: #fefce8; 202 + } 203 + .move-btn.featured:hover { background: #ca8a04; color: #fff; } 187 204 @media (prefers-color-scheme: dark) { 188 - .trust-icon { background: #1f2937; } 189 - .trust-icon.active { background: #052e16; } 205 + .move-btn.featured { background: #422006; } 190 206 } 191 207 .empty { text-align: center; padding: 3rem 1rem; color: var(--muted); } 192 208 footer { ··· 241 257 esc(sortUrl("first_seen")) 242 258 }">First seen${sortIndicator("first_seen")}</a></th> 243 259 <th class="hide-mobile">Trust</th> 260 + <th></th> 244 261 </tr> 245 262 </thead> 246 263 <tbody> ··· 250 267 : `<div class="empty">No servers found. Data may still be loading — check back after the first cron run.</div>` 251 268 } 252 269 <footer> 253 - <a href="/api/servers">JSON API</a> 254 - &middot; Source on <a href="https://github.com/tijs">GitHub</a> 270 + <p>Built by <a href="https://bsky.app/profile/tijs.org">@tijs.org</a></p> 271 + <p><a href="/api/servers">JSON API</a></p> 255 272 </footer> 256 273 </body> 257 274 </html>`; ··· 301 318 <span class="trust-icons"> 302 319 ${ 303 320 hasEmail 304 - ? `<span class="trust-icon active" title="Contact email">@</span>` 305 - : `<span class="trust-icon" title="No contact email">@</span>` 321 + ? `<span class="trust-icon" title="Contact: ${esc(hasEmail)}">@</span>` 322 + : "" 306 323 } 307 324 ${ 308 325 hasTos 309 - ? `<a class="trust-icon active" href="${ 326 + ? `<a class="trust-icon" href="${ 310 327 esc(hasTos) 311 - }" title="Terms of Service" target="_blank" rel="noopener">T</a>` 312 - : `<span class="trust-icon" title="No Terms of Service">T</span>` 328 + }" title="Terms of Service" target="_blank" rel="noopener">ToS</a>` 329 + : "" 313 330 } 314 331 ${ 315 332 hasPrivacy 316 - ? `<a class="trust-icon active" href="${ 333 + ? `<a class="trust-icon" href="${ 317 334 esc(hasPrivacy) 318 - }" title="Privacy Policy" target="_blank" rel="noopener">P</a>` 319 - : `<span class="trust-icon" title="No Privacy Policy">P</span>` 335 + }" title="Privacy Policy" target="_blank" rel="noopener">PP</a>` 336 + : "" 320 337 } 321 338 </span> 322 339 </td> 340 + <td><a class="move-btn${ 341 + hasEmail ? " featured" : "" 342 + }" href="https://pdsmoover.com/moover/${ 343 + esc(hostname) 344 + }" target="_blank" rel="noopener">Moove here</a></td> 323 345 </tr>`; 324 346 } 325 347
+1 -1
shared/constants.ts
··· 7 7 export const PDS_REPO_OWNER = "bluesky-social"; 8 8 export const PDS_REPO_NAME = "pds"; 9 9 10 - export const ENRICHMENT_BATCH_SIZE = 20; 10 + export const ENRICHMENT_BATCH_SIZE = 50; 11 11 export const PDS_REQUEST_TIMEOUT_MS = 5000; 12 12 13 13 /** Country code to flag emoji */