Standard.site landing page built in Next.js
0

Configure Feed

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

1import { AtpAgent } from "@atproto/api"; 2import { readFileSync, readdirSync, statSync } from "fs"; 3import { join, dirname } from "path"; 4import { fileURLToPath } from "url"; 5 6const __dirname = dirname(fileURLToPath(import.meta.url)); 7const COLLECTION = "site.standard.document"; 8const CONTENT_DIR = join(__dirname, "../content"); 9 10function extractKeepRkeys(dir: string): Set<string> { 11 const rkeys = new Set<string>(); 12 13 for (const entry of readdirSync(dir)) { 14 const fullPath = join(dir, entry); 15 if (statSync(fullPath).isDirectory()) { 16 for (const rkey of extractKeepRkeys(fullPath)) { 17 rkeys.add(rkey); 18 } 19 } else if (entry.endsWith(".mdx") || entry.endsWith(".md")) { 20 const content = readFileSync(fullPath, "utf-8"); 21 const match = content.match( 22 /atUri:\s*["']at:\/\/[^/]+\/site\.standard\.document\/([^"'\s]+)["']/ 23 ); 24 if (match) rkeys.add(match[1]); 25 } 26 } 27 28 return rkeys; 29} 30 31async function main() { 32 const handle = process.env.ATP_IDENTIFIER; 33 const password = process.env.ATP_APP_PASSWORD; 34 35 if (!handle || !password) { 36 console.error("ATP_IDENTIFIER and ATP_APP_PASSWORD must be set."); 37 process.exit(1); 38 } 39 40 const keepRkeys = extractKeepRkeys(CONTENT_DIR); 41 console.log(`Found ${keepRkeys.size} records to keep from frontmatter`); 42 43 const agent = new AtpAgent({ service: "https://bsky.social" }); 44 await agent.login({ identifier: handle, password }); 45 46 const did = agent.session!.did; 47 console.log(`Logged in as ${did}`); 48 49 // List all records 50 const duplicates: { rkey: string; title: string }[] = []; 51 let cursor: string | undefined; 52 53 do { 54 const res = await agent.com.atproto.repo.listRecords({ 55 repo: did, 56 collection: COLLECTION, 57 limit: 100, 58 cursor, 59 }); 60 61 for (const record of res.data.records) { 62 const rkey = record.uri.split("/").pop()!; 63 if (!keepRkeys.has(rkey)) { 64 const value = record.value as Record<string, unknown>; 65 duplicates.push({ rkey, title: (value.title as string) || "(untitled)" }); 66 } 67 } 68 69 cursor = res.data.cursor; 70 } while (cursor); 71 72 if (duplicates.length === 0) { 73 console.log("No duplicates found — PDS is clean."); 74 return; 75 } 76 77 console.log(`Removing ${duplicates.length} duplicate(s)...`); 78 79 for (const dup of duplicates) { 80 await agent.com.atproto.repo.deleteRecord({ 81 repo: did, 82 collection: COLLECTION, 83 rkey: dup.rkey, 84 }); 85 console.log(` ✅ Deleted: ${dup.title} (${dup.rkey})`); 86 } 87 88 console.log(`Removed ${duplicates.length} duplicate(s).`); 89} 90 91main().catch((err) => { 92 console.error("Error:", err); 93 process.exit(1); 94});