a standard.site publication renderer for SvelteKit.
at main 304 lines 7.5 kB view raw view rendered
1# Publishing to ATProto 2 3This guide explains how to publish content FROM your SvelteKit site TO the ATProto network (Bluesky, Leaflet, WhiteWind, etc.). 4 5## Prerequisites 6 71. A Bluesky account (or any ATProto account) 82. An app password (NOT your main password) 9 - Get one at: https://bsky.app/settings/app-passwords 103. Your DID (Decentralized Identifier) 11 - Find it at: https://bsky.app/settings 12 13## Quick Start 14 15### 1. Install Dependencies 16 17```bash 18pnpm add svelte-standard-site zod 19``` 20 21### 2. Create a Publication 22 23A publication represents your blog/site on ATProto. 24 25```typescript 26// scripts/create-publication.ts 27import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 28 29const publisher = new StandardSitePublisher({ 30 identifier: 'you.bsky.social', // or your DID 31 password: process.env.ATPROTO_APP_PASSWORD! 32}); 33 34await publisher.login(); 35 36const result = await publisher.publishPublication({ 37 name: 'My Awesome Blog', 38 url: 'https://yourblog.com', 39 description: 'Thoughts on code, life, and everything', 40 basicTheme: { 41 background: { r: 255, g: 245, b: 235 }, 42 foreground: { r: 30, g: 30, b: 30 }, 43 accent: { r: 74, g: 124, b: 155 }, 44 accentForeground: { r: 255, g: 255, b: 255 } 45 } 46}); 47 48console.log('Publication created!'); 49console.log('AT-URI:', result.uri); 50console.log('Save this rkey:', result.uri.split('/').pop()); 51``` 52 53Run it: 54 55```bash 56ATPROTO_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" node scripts/create-publication.ts 57``` 58 59### 3. Publish Documents 60 61Create a script to sync your blog posts to ATProto: 62 63```typescript 64// scripts/publish-posts.ts 65import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 66import { transformContent } from 'svelte-standard-site/content'; 67import fs from 'fs'; 68import matter from 'gray-matter'; 69 70const publisher = new StandardSitePublisher({ 71 identifier: 'you.bsky.social', 72 password: process.env.ATPROTO_APP_PASSWORD! 73}); 74 75await publisher.login(); 76 77// Read your markdown files 78const files = fs.readdirSync('./content/posts'); 79 80for (const file of files) { 81 const content = fs.readFileSync(`./content/posts/${file}`, 'utf-8'); 82 const { data, content: markdown } = matter(content); 83 84 // Transform content for ATProto 85 const transformed = transformContent(markdown, { 86 baseUrl: 'https://yourblog.com' 87 }); 88 89 // Publish to ATProto 90 const result = await publisher.publishDocument({ 91 site: 'https://yourblog.com', // or AT-URI of your publication 92 title: data.title, 93 description: data.description, 94 publishedAt: data.date.toISOString(), 95 path: `/posts/${file.replace('.md', '')}`, 96 tags: data.tags, 97 content: { 98 $type: 'site.standard.content.markdown', 99 text: transformed.markdown, 100 version: '1.0' 101 }, 102 textContent: transformed.textContent 103 }); 104 105 console.log(`Published: ${data.title}`); 106 console.log(`${result.uri}`); 107} 108``` 109 110## Advanced Usage 111 112### Update Existing Documents 113 114```typescript 115// Get the rkey from the original publish result 116const rkey = '3abc123xyz789'; 117 118await publisher.updateDocument(rkey, { 119 site: 'https://yourblog.com', 120 title: 'Updated Title', 121 publishedAt: originalDate.toISOString(), 122 updatedAt: new Date().toISOString(), 123 content: { 124 $type: 'site.standard.content.markdown', 125 text: updatedMarkdown 126 } 127}); 128``` 129 130### Delete Documents 131 132```typescript 133await publisher.deleteDocument('3abc123xyz789'); 134``` 135 136### List Your Published Documents 137 138```typescript 139const documents = await publisher.listDocuments(); 140 141for (const doc of documents) { 142 console.log(`${doc.value.title} - ${doc.uri}`); 143} 144``` 145 146### Custom Themes 147 148```typescript 149await publisher.publishPublication({ 150 name: 'Dark Mode Blog', 151 url: 'https://yourblog.com', 152 basicTheme: { 153 background: { r: 13, g: 17, b: 23 }, // Dark 154 foreground: { r: 230, g: 237, b: 243 }, // Light text 155 accent: { r: 136, g: 58, b: 234 }, // Purple 156 accentForeground: { r: 255, g: 255, b: 255 } 157 } 158}); 159``` 160 161### With Cover Images 162 163First, upload the image as a blob: 164 165```typescript 166const agent = publisher.getAtpAgent(); 167 168const imageBuffer = fs.readFileSync('./cover.jpg'); 169const uploadResult = await agent.uploadBlob(imageBuffer, { 170 encoding: 'image/jpeg' 171}); 172 173await publisher.publishDocument({ 174 // ...other fields 175 coverImage: { 176 $type: 'blob', 177 ref: { $link: uploadResult.data.blob.ref.$link }, 178 mimeType: 'image/jpeg', 179 size: imageBuffer.length 180 } 181}); 182``` 183 184## SvelteKit Integration 185 186### Create an Admin Route 187 188```typescript 189// src/routes/admin/publish/+page.server.ts 190import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 191import { env } from '$env/dynamic/private'; 192import { error } from '@sveltejs/kit'; 193import type { PageServerLoad, Actions } from './$types'; 194 195export const load: PageServerLoad = async () => { 196 // List existing documents 197 const publisher = new StandardSitePublisher({ 198 identifier: env.ATPROTO_HANDLE!, 199 password: env.ATPROTO_APP_PASSWORD! 200 }); 201 202 await publisher.login(); 203 const documents = await publisher.listDocuments(); 204 205 return { 206 documents 207 }; 208}; 209 210export const actions = { 211 publish: async ({ request }) => { 212 const data = await request.formData(); 213 const title = data.get('title') as string; 214 const content = data.get('content') as string; 215 216 const publisher = new StandardSitePublisher({ 217 identifier: env.ATPROTO_HANDLE!, 218 password: env.ATPROTO_APP_PASSWORD! 219 }); 220 221 await publisher.login(); 222 223 const result = await publisher.publishDocument({ 224 site: env.PUBLIC_SITE_URL!, 225 title, 226 publishedAt: new Date().toISOString(), 227 content: { 228 $type: 'site.standard.content.markdown', 229 text: content 230 } 231 }); 232 233 return { success: true, uri: result.uri }; 234 } 235} satisfies Actions; 236``` 237 238## Important Notes 239 240### Security 241 2421. **Never commit app passwords** - Use environment variables 2432. **Never use main password** - Always use app passwords 2443. **Validate input** - Always validate data before publishing 2454. **Rate limiting** - Be mindful of API rate limits 246 247### TID Format 248 249Record keys (rkeys) MUST be TIDs (Timestamp Identifiers). The publisher generates these automatically. Do not manually create rkeys. 250 251### PDS Resolution 252 253The publisher automatically resolves your PDS from your DID document. You don't need to specify it unless you're using a custom PDS. 254 255### Content Types 256 257The `content` field is an open union. Different platforms support different types: 258 259- `site.standard.content.markdown` - Markdown content 260- `site.standard.content.html` - HTML content 261- Platform-specific types 262 263Always include `textContent` for search/indexing. 264 265## Troubleshooting 266 267### "Failed to resolve handle" 268 269- Check your handle is correct 270- Verify your PDS is reachable 271- Ensure you're using an app password 272 273### "Schema validation failed" 274 275- Check your data matches the schema 276- Ensure dates are ISO 8601 format 277- Verify URLs are valid 278 279### "Invalid TID" 280 281- Don't manually create rkeys 282- Let the publisher generate TIDs automatically 283 284### "Authentication failed" 285 286- Verify your app password is correct 287- Check it hasn't been revoked 288- Ensure you're not using your main password 289 290## Best Practices 291 2921. **Use content transformation** - Always run markdown through `transformContent()` 2932. **Include textContent** - Provides plain text for search 2943. **Add descriptions** - Helps with discovery 2954. **Use tags** - Categorize your content 2965. **Set updatedAt** - Track when content changes 2976. **Link Bluesky posts** - Use `bskyPostRef` for engagement 2987. **Verify ownership** - Set up `.well-known` endpoints 299 300## Next Steps 301 302- [Content Transformation](./content-transformation.md) 303- [Verification](./verification.md) 304- [Comments](./comments.md)