From f6c113ca93ad29ffd5166a71972e1c129e964fa2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:46:33 +0000 Subject: [PATCH 1/9] Add standard.site support via @bryanguffey/astro-standard-site - Install @bryanguffey/astro-standard-site package - Add optional standardSite config to StarpodConfig type - Configure standardSite in starpod.config.ts with placeholder values - Create .well-known/site.standard.publication verification endpoint --- package.json | 1 + pnpm-lock.yaml | 138 ++++++++++++++++++ .../.well-known/site.standard.publication.ts | 19 +++ src/utils/config.ts | 16 ++ starpod.config.ts | 6 +- 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/pages/.well-known/site.standard.publication.ts diff --git a/package.json b/package.json index 3856403..a174465 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@astrojs/preact": "^5.1.4", "@astrojs/vercel": "^10.0.8", + "@bryanguffey/astro-standard-site": "^1.0.3", "@libsql/client": "^0.17.3", "@preact/signals": "^2.9.1", "@vercel/analytics": "^1.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eccea7e..389b06c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@astrojs/vercel': specifier: ^10.0.8 version: 10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(react@19.0.0)(rollup@4.60.4) + '@bryanguffey/astro-standard-site': + specifier: ^1.0.3 + version: 1.0.3(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0)) '@libsql/client': specifier: ^0.17.3 version: 0.17.3 @@ -217,6 +220,33 @@ packages: '@astrojs/yaml2ts@0.2.4': resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==} + '@atproto/api@0.15.27': + resolution: {integrity: sha512-ok/WGafh1nz4t8pEQGtAF/32x2E2VDWU4af6BajkO5Gky2jp2q6cv6aB2A5yuvNNcc3XkYMYipsqVHVwLPMF9g==} + + '@atproto/common-web@0.4.21': + resolution: {integrity: sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==} + + '@atproto/lex-data@0.0.15': + resolution: {integrity: sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==} + + '@atproto/lex-json@0.0.16': + resolution: {integrity: sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==} + + '@atproto/lexicon@0.4.14': + resolution: {integrity: sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==} + + '@atproto/lexicon@0.6.2': + resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==} + + '@atproto/syntax@0.4.3': + resolution: {integrity: sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==} + + '@atproto/syntax@0.5.4': + resolution: {integrity: sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==} + + '@atproto/xrpc@0.7.7': + resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -314,6 +344,11 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bryanguffey/astro-standard-site@1.0.3': + resolution: {integrity: sha512-+4//yk8rRD3ZfC9uOKY7OHu7Shm5qMZSgFTIcDs079hcoV42yF8mK8O0AI6QMFb7iMCPqN9rO+leP+PMEEmR3A==} + peerDependencies: + astro: ^5.0.0 + '@capsizecss/unpack@4.0.0': resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} engines: {node: '>=18'} @@ -1959,6 +1994,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} @@ -2896,6 +2934,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iso-datestring-validator@2.2.2: + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -3251,6 +3292,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3872,6 +3916,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} + hasBin: true + tldts-core@7.4.2: resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} @@ -3936,6 +3984,9 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + uint8arrays@3.0.0: + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + ultrahtml@1.6.0: resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} @@ -3945,6 +3996,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicode-segmenter@0.14.5: + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4388,6 +4442,9 @@ packages: zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -4564,6 +4621,65 @@ snapshots: dependencies: yaml: 2.9.0 + '@atproto/api@0.15.27': + dependencies: + '@atproto/common-web': 0.4.21 + '@atproto/lexicon': 0.4.14 + '@atproto/syntax': 0.4.3 + '@atproto/xrpc': 0.7.7 + await-lock: 2.2.2 + multiformats: 9.9.0 + tlds: 1.261.0 + zod: 3.25.76 + + '@atproto/common-web@0.4.21': + dependencies: + '@atproto/lex-data': 0.0.15 + '@atproto/lex-json': 0.0.16 + '@atproto/syntax': 0.5.4 + zod: 3.25.76 + + '@atproto/lex-data@0.0.15': + dependencies: + multiformats: 9.9.0 + tslib: 2.8.1 + uint8arrays: 3.0.0 + unicode-segmenter: 0.14.5 + + '@atproto/lex-json@0.0.16': + dependencies: + '@atproto/lex-data': 0.0.15 + tslib: 2.8.1 + + '@atproto/lexicon@0.4.14': + dependencies: + '@atproto/common-web': 0.4.21 + '@atproto/syntax': 0.4.3 + iso-datestring-validator: 2.2.2 + multiformats: 9.9.0 + zod: 3.25.76 + + '@atproto/lexicon@0.6.2': + dependencies: + '@atproto/common-web': 0.4.21 + '@atproto/syntax': 0.5.4 + iso-datestring-validator: 2.2.2 + multiformats: 9.9.0 + zod: 3.25.76 + + '@atproto/syntax@0.4.3': + dependencies: + tslib: 2.8.1 + + '@atproto/syntax@0.5.4': + dependencies: + tslib: 2.8.1 + + '@atproto/xrpc@0.7.7': + dependencies: + '@atproto/lexicon': 0.6.2 + zod: 3.25.76 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -4695,6 +4811,12 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bryanguffey/astro-standard-site@1.0.3(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@atproto/api': 0.15.27 + astro: 6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) + zod: 3.25.76 + '@capsizecss/unpack@4.0.0': dependencies: fontkitten: 1.0.3 @@ -6069,6 +6191,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + await-lock@2.2.2: {} + axios@1.16.1: dependencies: follow-redirects: 1.16.0 @@ -7076,6 +7200,8 @@ snapshots: isexe@2.0.0: {} + iso-datestring-validator@2.2.2: {} + jiti@2.7.0: {} js-base64@3.7.8: {} @@ -7591,6 +7717,8 @@ snapshots: muggle-string@0.4.1: {} + multiformats@9.9.0: {} + nanoid@3.3.12: {} natural-compare@1.4.0: {} @@ -8245,6 +8373,8 @@ snapshots: tinyrainbow@3.1.0: {} + tlds@1.261.0: {} + tldts-core@7.4.2: {} tldts@7.4.2: @@ -8297,12 +8427,18 @@ snapshots: ufo@1.6.4: {} + uint8arrays@3.0.0: + dependencies: + multiformats: 9.9.0 + ultrahtml@1.6.0: {} uncrypto@0.1.3: {} undici-types@7.16.0: {} + unicode-segmenter@0.14.5: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -8680,6 +8816,8 @@ snapshots: zimmerframe@1.1.4: {} + zod@3.25.76: {} + zod@4.4.3: {} zwitch@2.0.4: {} diff --git a/src/pages/.well-known/site.standard.publication.ts b/src/pages/.well-known/site.standard.publication.ts new file mode 100644 index 0000000..ee4e9b0 --- /dev/null +++ b/src/pages/.well-known/site.standard.publication.ts @@ -0,0 +1,19 @@ +import type { APIRoute } from 'astro'; +import { generatePublicationWellKnown } from '@bryanguffey/astro-standard-site'; +import starpodConfig from '../../../starpod.config'; + +export const GET: APIRoute = () => { + const { standardSite } = starpodConfig; + + if (!standardSite) { + return new Response('standard.site not configured', { status: 404 }); + } + + return new Response( + generatePublicationWellKnown({ + did: standardSite.did, + publicationRkey: standardSite.publicationRkey + }), + { headers: { 'Content-Type': 'text/plain' } } + ); +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index c24c53d..1f48dae 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -25,6 +25,17 @@ export type Host = { website?: string; }; +export type StandardSiteConfig = { + /** + * Your ATProto DID (e.g., "did:plc:abc123"). Find yours at https://bsky.app/settings. + */ + did: string; + /** + * The publication record key from when you created your publication on ATProto. + */ + publicationRkey: string; +}; + export type StarpodConfig = { /** * A very short tagline for your show. Generally, no more than one sentence. Less is more here. @@ -53,6 +64,11 @@ export type StarpodConfig = { * The url to the RSS feed where your podcast is hosted. */ rssFeed: string; + /** + * Configuration for standard.site (ATProto/Bluesky federation). + * When provided, enables the .well-known/site.standard.publication verification endpoint. + */ + standardSite?: StandardSiteConfig; }; export const defineStarpodConfig = (config: StarpodConfig) => config; diff --git a/starpod.config.ts b/starpod.config.ts index 5a38d9e..d250f47 100644 --- a/starpod.config.ts +++ b/starpod.config.ts @@ -32,5 +32,9 @@ export default defineStarpodConfig({ spotify: 'https://open.spotify.com/show/19jiuHAqzeKnkleQUpZxDf', youtube: 'https://www.youtube.com/@WhiskeyWebAndWhatnot/' }, - rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml' + rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml', + standardSite: { + did: 'did:plc:your-did-here', + publicationRkey: 'your-publication-rkey-here' + } }); From 51f2d0b4c2204662a4c1663363d2edce62d658fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:22:44 +0000 Subject: [PATCH 2/9] Use environment variables for standard.site config instead of starpod.config.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves DID and publication rkey to STANDARD_SITE_DID and STANDARD_SITE_PUBLICATION_RKEY env vars, matching the existing pattern (DISCORD_WEBHOOK). Removes StandardSiteConfig type and placeholder values from starpod.config.ts — values are only written once in .env. --- CLAUDE.md | 5 +++++ .../.well-known/site.standard.publication.ts | 11 ++++------- src/utils/config.ts | 16 ---------------- starpod.config.ts | 6 +----- 4 files changed, 10 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7bde5c1..8c5a9e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,3 +88,8 @@ configured for Preact (`jsxImportSource: "preact"`). - `ASTRO_DB_REMOTE_URL` — Turso/libSQL database URL (e.g., `libsql://your-db.turso.io`). - `ASTRO_DB_APP_TOKEN` — Authentication token for Turso database. +- `STANDARD_SITE_DID` — Your ATProto DID for standard.site verification (e.g., + `did:plc:abc123`). Find yours at https://bsky.app/settings. +- `STANDARD_SITE_PUBLICATION_RKEY` — The publication record key returned when + creating a publication via `StandardSitePublisher.publishPublication()` from + `@bryanguffey/astro-standard-site`. diff --git a/src/pages/.well-known/site.standard.publication.ts b/src/pages/.well-known/site.standard.publication.ts index ee4e9b0..985d50d 100644 --- a/src/pages/.well-known/site.standard.publication.ts +++ b/src/pages/.well-known/site.standard.publication.ts @@ -1,19 +1,16 @@ import type { APIRoute } from 'astro'; import { generatePublicationWellKnown } from '@bryanguffey/astro-standard-site'; -import starpodConfig from '../../../starpod.config'; export const GET: APIRoute = () => { - const { standardSite } = starpodConfig; + const did = import.meta.env.STANDARD_SITE_DID; + const publicationRkey = import.meta.env.STANDARD_SITE_PUBLICATION_RKEY; - if (!standardSite) { + if (!did || !publicationRkey) { return new Response('standard.site not configured', { status: 404 }); } return new Response( - generatePublicationWellKnown({ - did: standardSite.did, - publicationRkey: standardSite.publicationRkey - }), + generatePublicationWellKnown({ did, publicationRkey }), { headers: { 'Content-Type': 'text/plain' } } ); }; diff --git a/src/utils/config.ts b/src/utils/config.ts index 1f48dae..c24c53d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -25,17 +25,6 @@ export type Host = { website?: string; }; -export type StandardSiteConfig = { - /** - * Your ATProto DID (e.g., "did:plc:abc123"). Find yours at https://bsky.app/settings. - */ - did: string; - /** - * The publication record key from when you created your publication on ATProto. - */ - publicationRkey: string; -}; - export type StarpodConfig = { /** * A very short tagline for your show. Generally, no more than one sentence. Less is more here. @@ -64,11 +53,6 @@ export type StarpodConfig = { * The url to the RSS feed where your podcast is hosted. */ rssFeed: string; - /** - * Configuration for standard.site (ATProto/Bluesky federation). - * When provided, enables the .well-known/site.standard.publication verification endpoint. - */ - standardSite?: StandardSiteConfig; }; export const defineStarpodConfig = (config: StarpodConfig) => config; diff --git a/starpod.config.ts b/starpod.config.ts index d250f47..5a38d9e 100644 --- a/starpod.config.ts +++ b/starpod.config.ts @@ -32,9 +32,5 @@ export default defineStarpodConfig({ spotify: 'https://open.spotify.com/show/19jiuHAqzeKnkleQUpZxDf', youtube: 'https://www.youtube.com/@WhiskeyWebAndWhatnot/' }, - rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml', - standardSite: { - did: 'did:plc:your-did-here', - publicationRkey: 'your-publication-rkey-here' - } + rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml' }); From 9291cb1ce6538770b4c100684a238eb44939a46c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:10:25 +0000 Subject: [PATCH 3/9] Add standard.site documentation to README Explains what standard.site federation is, why you'd want it, how to configure the env vars, and links to further resources. Clarifies the feature is optional and the site works fine without it. --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index c3ae676..a41456c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,44 @@ environment variable it should work for you. Of course, feel free to customize the code [here](./src/pages/api/contact.ts) to send the data elsewhere as you see fit. +#### standard.site (ATProto Federation) + +Starpod supports [standard.site](https://standard.site/) — a specification that +connects your podcast website to [ATProto](https://atproto.com/) (the protocol +behind Bluesky). Enabling this allows: + +- **Verified ownership** — Cryptographically prove you own your content across + the federated web +- **Cross-platform discovery** — Your podcast appears on ATProto readers like + [Leaflet](https://leaflet.pub/) and [Pckt](https://pckt.blog) +- **Federated engagement** — Comments and interactions from Bluesky and other + ATProto apps can connect back to your site + +This feature is entirely optional. The site works perfectly without it — the +verification endpoint simply returns a 404 when unconfigured. No changes to +`astro.config.mjs` are needed. + +**To enable**, set these environment variables (e.g., in `.env`): + +``` +STANDARD_SITE_DID=did:plc:your-did-here +STANDARD_SITE_PUBLICATION_RKEY=your-publication-rkey-here +``` + +| Variable | Description | Where to find it | +|----------|-------------|------------------| +| `STANDARD_SITE_DID` | Your ATProto DID (decentralized identifier) | [bsky.app/settings](https://bsky.app/settings) → scroll to "DID" | +| `STANDARD_SITE_PUBLICATION_RKEY` | Record key for your publication | Returned by `StandardSitePublisher.publishPublication()` from [`@bryanguffey/astro-standard-site`](https://github.com/musicjunkieg/astro-standard-site) | + +After deploying, verify with: + +```bash +curl https://your-site.com/.well-known/site.standard.publication +``` + +For full setup instructions (creating a publication, syncing posts, etc.), see +the [`@bryanguffey/astro-standard-site` README](https://github.com/musicjunkieg/astro-standard-site#readme). + #### Configuring guests We use Turso and Astro DB to setup guests per episode. If you would also like to From 99b21dbe1cd5dc45d96c951595bfcb560e66736a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 04:15:19 +0000 Subject: [PATCH 4/9] Add ATProto episode publishing with GitHub Actions for auto-publish and backfill --- .github/workflows/backfill-episodes.yml | 39 ++++++ .github/workflows/publish-episodes.yml | 40 ++++++ CLAUDE.md | 9 +- README.md | 64 ++++++++- package.json | 2 + scripts/create-publication.ts | 54 ++++++++ scripts/publish-episodes.ts | 164 ++++++++++++++++++++++++ 7 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/backfill-episodes.yml create mode 100644 .github/workflows/publish-episodes.yml create mode 100644 scripts/create-publication.ts create mode 100644 scripts/publish-episodes.ts diff --git a/.github/workflows/backfill-episodes.yml b/.github/workflows/backfill-episodes.yml new file mode 100644 index 0000000..fbc42cd --- /dev/null +++ b/.github/workflows/backfill-episodes.yml @@ -0,0 +1,39 @@ +name: Backfill Episodes to ATProto + +on: + workflow_dispatch: + inputs: + confirm: + description: 'Type "backfill" to confirm publishing all episodes to ATProto' + required: true + type: string + +jobs: + backfill: + runs-on: ubuntu-latest + if: ${{ github.event.inputs.confirm == 'backfill' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm install + - name: Backfill all episodes + run: pnpm tsx scripts/publish-episodes.ts --backfill + env: + ATPROTO_HANDLE: ${{ secrets.ATPROTO_HANDLE }} + ATPROTO_APP_PASSWORD: ${{ secrets.ATPROTO_APP_PASSWORD }} + STANDARD_SITE_URL: ${{ secrets.STANDARD_SITE_URL }} + STANDARD_SITE_PUBLICATION_RKEY: ${{ secrets.STANDARD_SITE_PUBLICATION_RKEY }} diff --git a/.github/workflows/publish-episodes.yml b/.github/workflows/publish-episodes.yml new file mode 100644 index 0000000..620f254 --- /dev/null +++ b/.github/workflows/publish-episodes.yml @@ -0,0 +1,40 @@ +name: Publish Episodes to ATProto + +on: + # Run after the daily site rebuild to catch new episodes + workflow_run: + workflows: ["Rebuild Astro Site"] + types: [completed] + # Allow manual trigger + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + # Only run if the triggering workflow succeeded (or manual dispatch) + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm install + - name: Publish new episodes + run: pnpm tsx scripts/publish-episodes.ts + env: + ATPROTO_HANDLE: ${{ secrets.ATPROTO_HANDLE }} + ATPROTO_APP_PASSWORD: ${{ secrets.ATPROTO_APP_PASSWORD }} + STANDARD_SITE_URL: ${{ secrets.STANDARD_SITE_URL }} + STANDARD_SITE_PUBLICATION_RKEY: ${{ secrets.STANDARD_SITE_PUBLICATION_RKEY }} diff --git a/CLAUDE.md b/CLAUDE.md index 8c5a9e2..151d6c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,5 +91,10 @@ configured for Preact (`jsxImportSource: "preact"`). - `STANDARD_SITE_DID` — Your ATProto DID for standard.site verification (e.g., `did:plc:abc123`). Find yours at https://bsky.app/settings. - `STANDARD_SITE_PUBLICATION_RKEY` — The publication record key returned when - creating a publication via `StandardSitePublisher.publishPublication()` from - `@bryanguffey/astro-standard-site`. + creating a publication via `scripts/create-publication.ts`. +- `ATPROTO_HANDLE` — Your Bluesky handle (e.g., `you.bsky.social`) for + publishing episodes to ATProto. +- `ATPROTO_APP_PASSWORD` — App password for ATProto API access. Create at + https://bsky.app/settings/app-passwords. +- `STANDARD_SITE_URL` — Your podcast website URL (e.g., `https://whiskey.fm`) + used as the publication site when publishing documents. diff --git a/README.md b/README.md index a41456c..7516dc5 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ see fit. Starpod supports [standard.site](https://standard.site/) — a specification that connects your podcast website to [ATProto](https://atproto.com/) (the protocol -behind Bluesky). Enabling this allows: +behind Bluesky). Each episode is published as an individual document on the +federated web. Enabling this allows: - **Verified ownership** — Cryptographically prove you own your content across the federated web @@ -118,24 +119,73 @@ behind Bluesky). Enabling this allows: [Leaflet](https://leaflet.pub/) and [Pckt](https://pckt.blog) - **Federated engagement** — Comments and interactions from Bluesky and other ATProto apps can connect back to your site +- **Episode-level publishing** — Each episode is a standalone document in ATProto This feature is entirely optional. The site works perfectly without it — the verification endpoint simply returns a 404 when unconfigured. No changes to `astro.config.mjs` are needed. -**To enable**, set these environment variables (e.g., in `.env`): +##### Initial Setup +1. Create an [app password](https://bsky.app/settings/app-passwords) on Bluesky +2. Create your publication record (run once): + +```bash +ATPROTO_HANDLE=you.bsky.social \ +ATPROTO_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \ +STANDARD_SITE_URL=https://your-podcast.com \ +pnpm tsx scripts/create-publication.ts ``` -STANDARD_SITE_DID=did:plc:your-did-here -STANDARD_SITE_PUBLICATION_RKEY=your-publication-rkey-here -``` + +3. Save the output values as environment variables + +##### Environment Variables + +Set these in your `.env` file for local development and as **GitHub Actions +secrets** for automated publishing: | Variable | Description | Where to find it | |----------|-------------|------------------| | `STANDARD_SITE_DID` | Your ATProto DID (decentralized identifier) | [bsky.app/settings](https://bsky.app/settings) → scroll to "DID" | -| `STANDARD_SITE_PUBLICATION_RKEY` | Record key for your publication | Returned by `StandardSitePublisher.publishPublication()` from [`@bryanguffey/astro-standard-site`](https://github.com/musicjunkieg/astro-standard-site) | +| `STANDARD_SITE_PUBLICATION_RKEY` | Record key for your publication | Returned by `scripts/create-publication.ts` | +| `ATPROTO_HANDLE` | Your Bluesky handle (e.g., `you.bsky.social`) | Your Bluesky username | +| `ATPROTO_APP_PASSWORD` | App password for ATProto API access | [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords) | +| `STANDARD_SITE_URL` | Your podcast website URL (e.g., `https://whiskey.fm`) | Your deployed site URL | + +##### GitHub Actions Secrets + +Add the following secrets to your repository at **Settings → Secrets and +variables → Actions → New repository secret**: + +- `ATPROTO_HANDLE` +- `ATPROTO_APP_PASSWORD` +- `STANDARD_SITE_URL` +- `STANDARD_SITE_PUBLICATION_RKEY` +- `STANDARD_SITE_DID` + +##### Publishing Episodes + +Episodes are published to ATProto as individual documents automatically: + +- **Automatic** — The `Publish Episodes to ATProto` workflow runs after each + daily site rebuild and publishes any new episodes +- **Manual** — Trigger the workflow manually from the Actions tab +- **Backfill** — Use the `Backfill Episodes to ATProto` workflow (Actions tab → + Run workflow → type "backfill") to publish all existing episodes + +You can also publish locally: + +```bash +# Publish only new episodes +pnpm publish:episodes + +# Backfill all episodes +pnpm publish:episodes:backfill +``` + +##### Verification -After deploying, verify with: +After deploying, verify the well-known endpoint with: ```bash curl https://your-site.com/.well-known/site.standard.publication diff --git a/package.json b/package.json index a174465..da229ce 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "lint:fix": "eslint . --fix", "preview": "astro preview", "start": "astro dev", + "publish:episodes": "tsx scripts/publish-episodes.ts", + "publish:episodes:backfill": "tsx scripts/publish-episodes.ts --backfill", "test": "concurrently \"pnpm:test:*(!fix)\" --names \"test:\"", "test:e2e": "pnpm exec playwright test", "test:unit": "vitest" diff --git a/scripts/create-publication.ts b/scripts/create-publication.ts new file mode 100644 index 0000000..9b5b166 --- /dev/null +++ b/scripts/create-publication.ts @@ -0,0 +1,54 @@ +/** + * Create or update the podcast publication record on ATProto + * + * Run this once to set up your publication. The returned rkey should be + * saved as the STANDARD_SITE_PUBLICATION_RKEY environment variable. + * + * Required environment variables: + * ATPROTO_HANDLE - Your Bluesky handle + * ATPROTO_APP_PASSWORD - An app password + * STANDARD_SITE_URL - Your podcast site URL + * + * Usage: + * ATPROTO_HANDLE=you.bsky.social ATPROTO_APP_PASSWORD=xxxx STANDARD_SITE_URL=https://whiskey.fm pnpm tsx scripts/create-publication.ts + */ + +import { StandardSitePublisher } from '@bryanguffey/astro-standard-site'; +import starpodConfig from '../starpod.config'; + +const identifier = process.env.ATPROTO_HANDLE; +const password = process.env.ATPROTO_APP_PASSWORD; +const siteUrl = process.env.STANDARD_SITE_URL; + +if (!identifier || !password || !siteUrl) { + console.error( + 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL' + ); + process.exit(1); +} + +async function main() { + const publisher = new StandardSitePublisher({ + identifier, + password + }); + + await publisher.login(); + console.log(`✅ Logged in as ${publisher.getDid()}`); + + const result = await publisher.publishPublication({ + name: starpodConfig.blurb, + url: siteUrl, + description: starpodConfig.description + }); + + console.log('\n🎉 Publication created!'); + console.log(`AT-URI: ${result.uri}`); + console.log(`\nSave this as STANDARD_SITE_PUBLICATION_RKEY: ${result.uri.split('/').pop()}`); + console.log(`Save this as STANDARD_SITE_DID: ${publisher.getDid()}`); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts new file mode 100644 index 0000000..a68d144 --- /dev/null +++ b/scripts/publish-episodes.ts @@ -0,0 +1,164 @@ +/** + * Publish podcast episodes to ATProto via standard.site + * + * Each episode is published as an individual document record. + * Supports both incremental publishing (new episodes only) and + * full backfill (all episodes). + * + * Required environment variables: + * ATPROTO_HANDLE - Your Bluesky handle (e.g., your-handle.bsky.social) + * ATPROTO_APP_PASSWORD - An app password from bsky.app/settings/app-passwords + * STANDARD_SITE_URL - Your podcast site URL (e.g., https://whiskey.fm) + * STANDARD_SITE_PUBLICATION_RKEY - The publication record key + * + * Usage: + * pnpm publish:episodes # publish new episodes only + * pnpm publish:episodes:backfill # publish all episodes (backfill) + */ + +import { htmlToText } from 'html-to-text'; +import parseFeed from 'rss-to-json'; +import { array, number, object, optional, parse, string } from 'valibot'; + +import { + StandardSitePublisher, + type PublishDocumentInput +} from '@bryanguffey/astro-standard-site'; + +import starpodConfig from '../starpod.config'; +import { dasherize } from '../src/utils/dasherize'; + +const BACKFILL = process.argv.includes('--backfill'); + +const identifier = process.env.ATPROTO_HANDLE; +const password = process.env.ATPROTO_APP_PASSWORD; +const siteUrl = process.env.STANDARD_SITE_URL; +const publicationRkey = process.env.STANDARD_SITE_PUBLICATION_RKEY; + +if (!identifier || !password || !siteUrl || !publicationRkey) { + console.error( + 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL, STANDARD_SITE_PUBLICATION_RKEY' + ); + process.exit(1); +} + +const FeedSchema = object({ + items: array( + object({ + id: string(), + title: string(), + published: number(), + description: string(), + content_encoded: optional(string()), + itunes_duration: number(), + itunes_episode: optional(number()), + itunes_episodeType: string(), + itunes_image: optional(object({ href: optional(string()) })), + enclosures: array( + object({ + url: string(), + type: string() + }) + ) + }) + ) +}); + +interface FeedEpisode { + id: string; + title: string; + published: number; + description: string; + content_encoded?: string; + itunes_duration: number; + itunes_episode?: number; + itunes_episodeType: string; +} + +async function main() { + console.log( + BACKFILL ? '📚 Backfilling all episodes...' : '🆕 Publishing new episodes...' + ); + + // Fetch episodes from RSS + // @ts-expect-error rss-to-json types don't match runtime API + const feed = await parseFeed.parse(starpodConfig.rssFeed); + const { items } = parse(FeedSchema, feed); + + const episodes = items.filter( + (item) => item.itunes_episodeType !== 'trailer' + ); + + // Initialize publisher + const publisher = new StandardSitePublisher({ + identifier, + password + }); + + await publisher.login(); + console.log(`✅ Logged in as ${publisher.getDid()}`); + + // Get existing documents to avoid duplicates + const existingDocs = await publisher.listDocuments(100); + const existingPaths = new Set( + existingDocs + .map((doc) => doc.value.path) + .filter(Boolean) + ); + + let published = 0; + let skipped = 0; + + for (const episode of episodes) { + const slug = dasherize(episode.title); + const path = `/${slug}`; + + if (!BACKFILL && existingPaths.has(path)) { + skipped++; + continue; + } + + const description = htmlToText(episode.description, { + wordwrap: false + }).slice(0, 300); + + const textContent = episode.content_encoded + ? htmlToText(episode.content_encoded, { wordwrap: false }) + : description; + + const input: PublishDocumentInput = { + site: siteUrl, + path, + title: episode.title, + description, + publishedAt: new Date(episode.published).toISOString(), + textContent, + tags: ['podcast'] + }; + + try { + const result = await publisher.publishDocument(input); + console.log(` ✅ ${episode.title}`); + console.log(` → ${result.uri}`); + published++; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + // Skip duplicates gracefully during backfill + if (message.includes('already exists') || message.includes('duplicate')) { + console.log(` ⏭️ ${episode.title} (already exists)`); + skipped++; + } else { + console.error(` ❌ ${episode.title}: ${message}`); + } + } + } + + console.log( + `\n🎉 Done! Published: ${published}, Skipped: ${skipped}, Total episodes: ${episodes.length}` + ); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); From c87dd2a1e0ce4ef8ed036513275af6a9c58cac38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 04:16:42 +0000 Subject: [PATCH 5/9] Add permissions blocks to ATProto publishing workflows --- .github/workflows/backfill-episodes.yml | 2 ++ .github/workflows/publish-episodes.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/backfill-episodes.yml b/.github/workflows/backfill-episodes.yml index fbc42cd..23b8d81 100644 --- a/.github/workflows/backfill-episodes.yml +++ b/.github/workflows/backfill-episodes.yml @@ -11,6 +11,8 @@ on: jobs: backfill: runs-on: ubuntu-latest + permissions: + contents: read if: ${{ github.event.inputs.confirm == 'backfill' }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-episodes.yml b/.github/workflows/publish-episodes.yml index 620f254..2ace86a 100644 --- a/.github/workflows/publish-episodes.yml +++ b/.github/workflows/publish-episodes.yml @@ -11,6 +11,8 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + contents: read # Only run if the triggering workflow succeeded (or manual dispatch) if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} steps: From 7f3202b7d8808f76cdeaee5d732b0ddc51ab903e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 04:21:52 +0000 Subject: [PATCH 6/9] Fix pagination: fetch all existing ATProto documents via cursor-based pagination --- scripts/publish-episodes.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index a68d144..d119482 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -99,12 +99,28 @@ async function main() { console.log(`✅ Logged in as ${publisher.getDid()}`); // Get existing documents to avoid duplicates - const existingDocs = await publisher.listDocuments(100); - const existingPaths = new Set( - existingDocs - .map((doc) => doc.value.path) - .filter(Boolean) - ); + const existingPaths = new Set(); + let cursor: string | undefined; + + // Paginate through all existing documents (ATProto caps at 100 per request) + do { + const agent = publisher.getAtpAgent(); + const response = await agent.api.com.atproto.repo.listRecords({ + repo: publisher.getDid(), + collection: 'site.standard.document', + limit: 100, + cursor + }); + + for (const record of response.data.records) { + const doc = record.value as { path?: string }; + if (doc.path) { + existingPaths.add(doc.path); + } + } + + cursor = response.data.cursor; + } while (cursor); let published = 0; let skipped = 0; From 3c188fcc5b72948765b1786c8fb9287e3e66a710 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 06:08:42 +0000 Subject: [PATCH 7/9] Make itunes_episodeType optional in publish-episodes FeedSchema for CI resilience --- scripts/publish-episodes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index d119482..5bff27b 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -52,7 +52,7 @@ const FeedSchema = object({ content_encoded: optional(string()), itunes_duration: number(), itunes_episode: optional(number()), - itunes_episodeType: string(), + itunes_episodeType: optional(string()), itunes_image: optional(object({ href: optional(string()) })), enclosures: array( object({ @@ -72,7 +72,7 @@ interface FeedEpisode { content_encoded?: string; itunes_duration: number; itunes_episode?: number; - itunes_episodeType: string; + itunes_episodeType?: string; } async function main() { From d29473526066b58a8554dffb0647271615eae7b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:55:31 +0000 Subject: [PATCH 8/9] Fix type errors and remove unused interface in publishing scripts --- scripts/create-publication.ts | 6 +++--- scripts/publish-episodes.ts | 19 ++++--------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/scripts/create-publication.ts b/scripts/create-publication.ts index 9b5b166..1c5cdee 100644 --- a/scripts/create-publication.ts +++ b/scripts/create-publication.ts @@ -29,8 +29,8 @@ if (!identifier || !password || !siteUrl) { async function main() { const publisher = new StandardSitePublisher({ - identifier, - password + identifier: identifier as string, + password: password as string }); await publisher.login(); @@ -38,7 +38,7 @@ async function main() { const result = await publisher.publishPublication({ name: starpodConfig.blurb, - url: siteUrl, + url: siteUrl as string, description: starpodConfig.description }); diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index 5bff27b..1989936 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -64,17 +64,6 @@ const FeedSchema = object({ ) }); -interface FeedEpisode { - id: string; - title: string; - published: number; - description: string; - content_encoded?: string; - itunes_duration: number; - itunes_episode?: number; - itunes_episodeType?: string; -} - async function main() { console.log( BACKFILL ? '📚 Backfilling all episodes...' : '🆕 Publishing new episodes...' @@ -91,8 +80,8 @@ async function main() { // Initialize publisher const publisher = new StandardSitePublisher({ - identifier, - password + identifier: identifier as string, + password: password as string }); await publisher.login(); @@ -105,7 +94,7 @@ async function main() { // Paginate through all existing documents (ATProto caps at 100 per request) do { const agent = publisher.getAtpAgent(); - const response = await agent.api.com.atproto.repo.listRecords({ + const response = await agent.com.atproto.repo.listRecords({ repo: publisher.getDid(), collection: 'site.standard.document', limit: 100, @@ -143,7 +132,7 @@ async function main() { : description; const input: PublishDocumentInput = { - site: siteUrl, + site: siteUrl as string, path, title: episode.title, description, From a133f59643347eab94432193bfc59eb4a1cff985 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:06:28 +0000 Subject: [PATCH 9/9] Move env var reads inside main() for proper type narrowing, remove string casts --- scripts/create-publication.ts | 28 ++++++++++++++-------------- scripts/publish-episodes.ts | 30 +++++++++++++++--------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/scripts/create-publication.ts b/scripts/create-publication.ts index 1c5cdee..948f08e 100644 --- a/scripts/create-publication.ts +++ b/scripts/create-publication.ts @@ -16,21 +16,21 @@ import { StandardSitePublisher } from '@bryanguffey/astro-standard-site'; import starpodConfig from '../starpod.config'; -const identifier = process.env.ATPROTO_HANDLE; -const password = process.env.ATPROTO_APP_PASSWORD; -const siteUrl = process.env.STANDARD_SITE_URL; - -if (!identifier || !password || !siteUrl) { - console.error( - 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL' - ); - process.exit(1); -} - async function main() { + const identifier = process.env.ATPROTO_HANDLE; + const password = process.env.ATPROTO_APP_PASSWORD; + const siteUrl = process.env.STANDARD_SITE_URL; + + if (!identifier || !password || !siteUrl) { + console.error( + 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL' + ); + process.exit(1); + } + const publisher = new StandardSitePublisher({ - identifier: identifier as string, - password: password as string + identifier, + password }); await publisher.login(); @@ -38,7 +38,7 @@ async function main() { const result = await publisher.publishPublication({ name: starpodConfig.blurb, - url: siteUrl as string, + url: siteUrl, description: starpodConfig.description }); diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index 1989936..dc0a62a 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -30,18 +30,6 @@ import { dasherize } from '../src/utils/dasherize'; const BACKFILL = process.argv.includes('--backfill'); -const identifier = process.env.ATPROTO_HANDLE; -const password = process.env.ATPROTO_APP_PASSWORD; -const siteUrl = process.env.STANDARD_SITE_URL; -const publicationRkey = process.env.STANDARD_SITE_PUBLICATION_RKEY; - -if (!identifier || !password || !siteUrl || !publicationRkey) { - console.error( - 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL, STANDARD_SITE_PUBLICATION_RKEY' - ); - process.exit(1); -} - const FeedSchema = object({ items: array( object({ @@ -65,6 +53,18 @@ const FeedSchema = object({ }); async function main() { + const identifier = process.env.ATPROTO_HANDLE; + const password = process.env.ATPROTO_APP_PASSWORD; + const siteUrl = process.env.STANDARD_SITE_URL; + const publicationRkey = process.env.STANDARD_SITE_PUBLICATION_RKEY; + + if (!identifier || !password || !siteUrl || !publicationRkey) { + console.error( + 'Missing required environment variables. Need: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, STANDARD_SITE_URL, STANDARD_SITE_PUBLICATION_RKEY' + ); + process.exit(1); + } + console.log( BACKFILL ? '📚 Backfilling all episodes...' : '🆕 Publishing new episodes...' ); @@ -80,8 +80,8 @@ async function main() { // Initialize publisher const publisher = new StandardSitePublisher({ - identifier: identifier as string, - password: password as string + identifier, + password }); await publisher.login(); @@ -132,7 +132,7 @@ async function main() { : description; const input: PublishDocumentInput = { - site: siteUrl as string, + site: siteUrl, path, title: episode.title, description,