a standard.site publication renderer for SvelteKit.
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)