feat: added images to atproto uploads
eb358c54
4 file(s) · +102 −5
| 30 | 30 | "@biomejs/biome": "2.1.1", |
|
| 31 | 31 | "@tailwindcss/aspect-ratio": "^0.4.2", |
|
| 32 | 32 | "@tailwindcss/typography": "^0.5.8", |
|
| 33 | + | "@types/bun": "^1.3.6", |
|
| 33 | 34 | "@types/markdown-it": "^14.1.2", |
|
| 34 | 35 | "@types/sanitize-html": "^2.16.0", |
|
| 35 | 36 | "autoprefixer": "^10.4.13", |
|
| 388 | 389 | ||
| 389 | 390 | "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], |
|
| 390 | 391 | ||
| 392 | + | "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], |
|
| 393 | + | ||
| 391 | 394 | "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], |
|
| 392 | 395 | ||
| 393 | 396 | "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], |
|
| 485 | 488 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
|
| 486 | 489 | ||
| 487 | 490 | "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], |
|
| 491 | + | ||
| 492 | + | "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], |
|
| 488 | 493 | ||
| 489 | 494 | "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], |
|
| 490 | 495 | ||
| 20 | 20 | "@biomejs/biome": "2.1.1", |
|
| 21 | 21 | "@tailwindcss/aspect-ratio": "^0.4.2", |
|
| 22 | 22 | "@tailwindcss/typography": "^0.5.8", |
|
| 23 | + | "@types/bun": "^1.3.6", |
|
| 23 | 24 | "@types/markdown-it": "^14.1.2", |
|
| 24 | 25 | "@types/sanitize-html": "^2.16.0", |
|
| 25 | 26 | "autoprefixer": "^10.4.13", |
| 20 | 20 | atUri?: string; |
|
| 21 | 21 | } |
|
| 22 | 22 | ||
| 23 | + | interface BlobRef { |
|
| 24 | + | $link: string; |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | interface BlobObject { |
|
| 28 | + | $type: "blob"; |
|
| 29 | + | ref: BlobRef; |
|
| 30 | + | mimeType: string; |
|
| 31 | + | size: number; |
|
| 32 | + | } |
|
| 33 | + | ||
| 23 | 34 | interface BlogPost { |
|
| 24 | 35 | filePath: string; |
|
| 25 | 36 | slug: string; |
|
| 148 | 159 | return `${beforeEnd}atUri: "${atUri}"\n${afterEnd}`; |
|
| 149 | 160 | } |
|
| 150 | 161 | ||
| 162 | + | async function uploadImageToPDS( |
|
| 163 | + | agent: AtpAgent, |
|
| 164 | + | imagePath: string, |
|
| 165 | + | ): Promise<BlobObject | undefined> { |
|
| 166 | + | if (!imagePath || !fs.existsSync(imagePath)) { |
|
| 167 | + | return undefined; |
|
| 168 | + | } |
|
| 169 | + | ||
| 170 | + | try { |
|
| 171 | + | // Use Bun's built-in file type detection |
|
| 172 | + | const file = Bun.file(imagePath); |
|
| 173 | + | const imageBuffer = await file.arrayBuffer(); |
|
| 174 | + | const mimeType = file.type || "application/octet-stream"; |
|
| 175 | + | ||
| 176 | + | const response = await agent.com.atproto.repo.uploadBlob( |
|
| 177 | + | new Uint8Array(imageBuffer), |
|
| 178 | + | { |
|
| 179 | + | encoding: mimeType, |
|
| 180 | + | }, |
|
| 181 | + | ); |
|
| 182 | + | ||
| 183 | + | console.log(response); |
|
| 184 | + | ||
| 185 | + | return { |
|
| 186 | + | $type: "blob", |
|
| 187 | + | ref: { |
|
| 188 | + | $link: response.data.blob.ref.toString(), |
|
| 189 | + | }, |
|
| 190 | + | mimeType, |
|
| 191 | + | size: imageBuffer.byteLength, |
|
| 192 | + | }; |
|
| 193 | + | } catch (error) { |
|
| 194 | + | console.error(`Error uploading image ${imagePath}:`, error); |
|
| 195 | + | return undefined; |
|
| 196 | + | } |
|
| 197 | + | } |
|
| 198 | + | ||
| 199 | + | function resolveImagePath(ogImage: string): string { |
|
| 200 | + | // Extract just the filename from the ogImage path |
|
| 201 | + | const filename = path.basename(ogImage); |
|
| 202 | + | ||
| 203 | + | // All blog images are stored in packages/client/public/blog-images/other |
|
| 204 | + | const imagePath = path.join(import.meta.dir, "../public/blog-images/other", filename); |
|
| 205 | + | ||
| 206 | + | if (!fs.existsSync(imagePath)) { |
|
| 207 | + | throw new Error(`Image not found: ${imagePath}`); |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | return imagePath; |
|
| 211 | + | } |
|
| 212 | + | ||
| 151 | 213 | async function createAtProtoDocument( |
|
| 152 | 214 | agent: AtpAgent, |
|
| 153 | 215 | post: BlogPost, |
|
| 163 | 225 | // Parse the publish date |
|
| 164 | 226 | const publishDate = new Date(post.frontmatter.publishDate); |
|
| 165 | 227 | ||
| 228 | + | // Handle cover image upload |
|
| 229 | + | let coverImage: BlobObject | undefined; |
|
| 230 | + | if (post.frontmatter.ogImage) { |
|
| 231 | + | const imagePath = resolveImagePath(post.frontmatter.ogImage); |
|
| 232 | + | console.log(` - Uploading cover image: ${imagePath}`); |
|
| 233 | + | coverImage = await uploadImageToPDS(agent, imagePath); |
|
| 234 | + | if (coverImage) { |
|
| 235 | + | console.log(` - Uploaded image blob: ${coverImage.ref.$link}`); |
|
| 236 | + | } |
|
| 237 | + | } |
|
| 238 | + | ||
| 166 | 239 | const record = { |
|
| 167 | 240 | $type: "site.standard.document", |
|
| 168 | 241 | title: post.frontmatter.title, |
|
| 169 | 242 | site: PUBLICATION_URI, |
|
| 170 | 243 | path: postPath, |
|
| 171 | 244 | content: markdownContent, |
|
| 172 | - | coverImage: post.frontmatter.ogImage, |
|
| 245 | + | coverImage, |
|
| 173 | 246 | textContent: textContent.slice(0, 10000), // Limit text content length |
|
| 174 | 247 | publishedAt: publishDate.toISOString(), |
|
| 175 | 248 | canonicalUrl: `${SITE_URL}${postPath}`, |
|
| 190 | 263 | post: BlogPost, |
|
| 191 | 264 | atUri: string, |
|
| 192 | 265 | ): Promise<void> { |
|
| 193 | - | // Parse the atUri to get the repo, collection, and rkey |
|
| 266 | + | // Parse the atUri to get the collection and rkey |
|
| 194 | 267 | // Format: at://did:plc:xxx/collection/rkey |
|
| 195 | 268 | const uriMatch = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); |
|
| 196 | 269 | if (!uriMatch) { |
|
| 197 | 270 | throw new Error(`Invalid atUri format: ${atUri}`); |
|
| 198 | 271 | } |
|
| 199 | 272 | ||
| 200 | - | const [, repo, collection, rkey] = uriMatch; |
|
| 273 | + | const [, , collection, rkey] = uriMatch; |
|
| 201 | 274 | ||
| 202 | 275 | const postPath = `/posts/${post.slug}`; |
|
| 203 | 276 | const markdownContent = { |
|
| 210 | 283 | // Parse the publish date |
|
| 211 | 284 | const publishDate = new Date(post.frontmatter.publishDate); |
|
| 212 | 285 | ||
| 286 | + | // Handle cover image upload |
|
| 287 | + | let coverImage: BlobObject | undefined; |
|
| 288 | + | if (post.frontmatter.ogImage) { |
|
| 289 | + | const imagePath = resolveImagePath(post.frontmatter.ogImage); |
|
| 290 | + | console.log(` - Uploading cover image: ${imagePath}`); |
|
| 291 | + | coverImage = await uploadImageToPDS(agent, imagePath); |
|
| 292 | + | if (coverImage) { |
|
| 293 | + | console.log(` - Uploaded image blob: ${coverImage.ref.$link}`); |
|
| 294 | + | } |
|
| 295 | + | } |
|
| 296 | + | ||
| 213 | 297 | const record = { |
|
| 214 | 298 | $type: "site.standard.document", |
|
| 215 | 299 | title: post.frontmatter.title, |
|
| 216 | 300 | site: PUBLICATION_URI, |
|
| 217 | 301 | path: postPath, |
|
| 218 | 302 | content: markdownContent, |
|
| 219 | - | coverImage: post.frontmatter.ogImage, |
|
| 303 | + | coverImage, |
|
| 220 | 304 | textContent: textContent.slice(0, 10000), // Limit text content length |
|
| 221 | 305 | publishedAt: publishDate.toISOString(), |
|
| 222 | 306 | canonicalUrl: `${SITE_URL}${postPath}`, |
|
| 151 | 151 | site: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.publication/3mbykzswhqc2x", |
|
| 152 | 152 | ...(normalizedPath && { path: normalizedPath.trim() }), |
|
| 153 | 153 | content: markdownContent, |
|
| 154 | - | coverImage: "https://stevedylan.dev/icon.png", |
|
| 154 | + | coverImage: { |
|
| 155 | + | type: "blob", |
|
| 156 | + | ref: { |
|
| 157 | + | link: "bafkreibuxyp2gth3igqik7fxu4cm4nducetgp67hhlx36bwahgnuw4xmoa", |
|
| 158 | + | }, |
|
| 159 | + | mimeType: "image/png", |
|
| 160 | + | size: 2522, |
|
| 161 | + | }, |
|
| 155 | 162 | textContent: textContent, |
|
| 156 | 163 | publishedAt: new Date().toISOString(), |
|
| 157 | 164 | }, |