// A photo gallery app: Grain records rendered live from whichever repo // serves it. Deploy it to your own PDS with PDS_URL, PDS_DID and // PDS_APP_PASSWORD set; see docs/apps.md. Gallery data // comes from dev.pdsjs.query.run at page load; images come from the site's // /.blobs/ route. The site record carries one file, so a new Grain // gallery appears without a redeploy. // // Usage: PDS_APP_PASSWORD=... node build-photos-site.mjs const PDS = process.env.PDS_URL; const DID = process.env.PDS_DID; if (!PDS || !DID) throw new Error('Set PDS_URL and PDS_DID.'); const SITE = 'photos-app'; const SITES = 'dev.pdsjs.site.deploy'; const DEFS = 'dev.pdsjs.query.def'; async function xrpc(path, init = {}) { const response = await fetch(`${PDS}/xrpc/${path}`, init); if (!response.ok) { throw new Error( `${path.split('?')[0]} ${response.status}: ${(await response.text()).slice(0, 200)}`, ); } return response.json(); } const password = process.env.PDS_APP_PASSWORD; if (!password) throw new Error('Set PDS_APP_PASSWORD.'); const session = await xrpc('com.atproto.server.createSession', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: DID, password }), }); const auth = { Authorization: `Bearer ${session.accessJwt}` }; /** @param {string} collection @param {string} rkey @param {Record} record */ async function put(collection, rkey, record) { await xrpc('com.atproto.repo.putRecord', { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ repo: DID, collection, rkey, record: { $type: collection, ...record }, }), }); console.error(`put ${collection}/${rkey}`); } // ---- the queries the app reads ---- const now = new Date().toISOString(); await put(DEFS, 'grain-galleries', { collection: 'social.grain.gallery', sort: { field: 'createdAt', direction: 'desc' }, createdAt: now, }); await put(DEFS, 'grain-gallery-items', { collection: 'social.grain.gallery.item', createdAt: now, }); await put(DEFS, 'grain-photos', { collection: 'social.grain.photo', createdAt: now, }); // ---- the app ---- const HTML = ` Photos

Loading galleries…

`; const upload = await xrpc('com.atproto.repo.uploadBlob', { method: 'POST', headers: { ...auth, 'Content-Type': 'text/html; charset=utf-8' }, body: HTML, }); await put(SITES, SITE, { files: [ { path: 'index.html', blob: upload.blob, contentType: 'text/html; charset=utf-8', cacheControl: 'public, max-age=300', }, ], fallback: 'index.html', createdAt: new Date().toISOString(), }); console.error( 'Deployed photos as a live SPA: 1 file, data and images read from the repo at view time.', ); // ---- publish the app ---- // The app's icon: its own layout, in miniature. const ICON_SVG = ` `; const iconUp = await fetch(`${PDS}/xrpc/com.atproto.repo.uploadBlob`, { method: 'POST', headers: { ...auth, 'Content-Type': 'image/svg+xml' }, body: ICON_SVG, }); const icon = (await iconUp.json()).blob; console.error('uploaded icon', icon.ref.$link); const deployed = await xrpc( `com.atproto.repo.getRecord?repo=${DID}&collection=${SITES}&rkey=${SITE}`, ); await put('dev.pdsjs.app.manifest', 'photos', { name: 'Photos', icon, suggestedSite: 'photos', description: 'Photo galleries from your Grain records, served straight from your own repo: justified layouts, a lightbox, terrain headers for located galleries, a map view, and favorite counts from the network.', site: { uri: deployed.uri, cid: deployed.cid }, primitives: ['serve', 'query'], external: true, queries: [ { name: 'grain-galleries', collection: 'social.grain.gallery', sort: { field: 'createdAt', direction: 'desc' }, }, { name: 'grain-gallery-items', collection: 'social.grain.gallery.item' }, { name: 'grain-photos', collection: 'social.grain.photo' }, ], settings: [ { key: 'topo', label: 'Terrain behind gallery headers', type: 'boolean', default: true, }, { key: 'map', label: 'Map view', type: 'boolean', default: true }, { key: 'social', label: 'Favorite and comment counts (asks microcosm.blue)', type: 'boolean', default: true, }, { key: 'rows', label: 'Photo row height', type: 'select', options: ['compact', 'roomy'], default: 'roomy', }, ], createdAt: new Date().toISOString(), }); console.error( 'Published dev.pdsjs.app.manifest/photos pinned to the deployed site.', );