A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import type { Agent } from '@atproto/api';
2import type { BlockNode } from '../blocks/render';
3import {
4 attachBlobRefs,
5 buildGetBlobUrl,
6 type BlobRefJson,
7 type BlobUpload,
8} from '../media/blob';
9import { isDataUrl, dataUrlToBlob } from './held-assets';
10
11export interface PreparedPublishContent {
12 blocks: BlockNode[];
13 coverImage?: BlobRefJson;
14}
15
16/** Upload one held `data:` URL to the PDS and return its portable blob ref + getBlob URL. */
17async function uploadOne(
18 agent: Agent,
19 dataUrl: string,
20 did: string,
21 pdsUrl: string
22): Promise< BlobUpload > {
23 const blob = dataUrlToBlob( dataUrl );
24 const res = await agent.uploadBlob( blob, { encoding: blob.type } );
25 const out = res.data.blob;
26 const cid = out.ref.toString();
27 return {
28 ref: { $type: 'blob', ref: { $link: cid }, mimeType: out.mimeType, size: out.size },
29 url: buildGetBlobUrl( pdsUrl, did, cid ),
30 };
31}
32
33/**
34 * Publish-time bridge for the writing-first flow: walk the block tree, upload every held
35 * (`data:`) image to the writer's PDS, and rewrite those blocks via `attachBlobRefs` so they
36 * carry `skypressBlob` + a portable getBlob URL (byte-identical to the eager path). External
37 * image URLs are left untouched. A held cover is uploaded into a `BlobRefJson` for the document
38 * record. Each distinct data URL uploads once.
39 */
40export async function uploadHeldAssets(
41 agent: Agent,
42 input: { blocks: BlockNode[]; coverDataUrl: string | null; did: string; pdsUrl: string }
43): Promise< PreparedPublishContent > {
44 const { blocks, coverDataUrl, did, pdsUrl } = input;
45
46 // Collect every distinct held image URL in the tree (depth-first), upload once each.
47 const registry = new Map< string, BlobUpload >();
48 const collect = ( nodes: BlockNode[] ): string[] =>
49 nodes.flatMap( ( node ) => {
50 const url = node.attributes?.url;
51 const here = node.name === 'core/image' && isDataUrl( url ) ? [ url as string ] : [];
52 return [ ...here, ...collect( node.innerBlocks ?? [] ) ];
53 } );
54
55 for ( const url of new Set( collect( blocks ) ) ) {
56 registry.set( url, await uploadOne( agent, url, did, pdsUrl ) );
57 }
58
59 const prepared = attachBlobRefs( blocks, ( url ) => registry.get( url ) );
60
61 let coverImage: BlobRefJson | undefined;
62 if ( isDataUrl( coverDataUrl ) ) {
63 coverImage = ( await uploadOne( agent, coverDataUrl, did, pdsUrl ) ).ref;
64 }
65
66 return { blocks: prepared, coverImage };
67}