···11+# Required: Your AT Protocol DID
22+PUBLIC_ATPROTO_DID=did:plc:revjuqmkvrw6fnkxppqtszpv
33+44+# Optional: Custom PDS endpoint (will be auto-resolved if not provided)
55+# PUBLIC_ATPROTO_PDS=https://cortinarius.us-west.host.bsky.network
66+77+# Optional: Cache TTL in milliseconds (default: 300000 = 5 minutes)
88+# PUBLIC_CACHE_TTL=300000
+628
API.md
···11+# API Reference
22+33+Complete API documentation for svelte-standard-site.
44+55+## Table of Contents
66+77+- [SiteStandardClient](#sitestandardclient)
88+- [Types](#types)
99+- [Utility Functions](#utility-functions)
1010+- [Components](#components)
1111+- [Configuration](#configuration)
1212+1313+## SiteStandardClient
1414+1515+The main client class for interacting with site.standard.\* records.
1616+1717+### Constructor
1818+1919+```typescript
2020+constructor(config: SiteStandardConfig)
2121+```
2222+2323+Creates a new instance of the client.
2424+2525+**Parameters:**
2626+2727+- `config` (SiteStandardConfig): Configuration object
2828+2929+**Example:**
3030+3131+```typescript
3232+import { createClient } from 'svelte-standard-site';
3333+3434+const client = createClient({
3535+ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv',
3636+ pds: 'https://cortinarius.us-west.host.bsky.network', // optional
3737+ cacheTTL: 300000 // optional, in ms
3838+});
3939+```
4040+4141+### Methods
4242+4343+#### `fetchPublication`
4444+4545+```typescript
4646+async fetchPublication(
4747+ rkey: string,
4848+ fetchFn?: typeof fetch
4949+): Promise<AtProtoRecord<Publication> | null>
5050+```
5151+5252+Fetches a single publication by its record key.
5353+5454+**Parameters:**
5555+5656+- `rkey` (string): The record key (TID) of the publication
5757+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
5858+5959+**Returns:** Promise resolving to the publication record or null if not found
6060+6161+**Example:**
6262+6363+```typescript
6464+const pub = await client.fetchPublication('3lwafzkjqm25s');
6565+if (pub) {
6666+ console.log(pub.value.name);
6767+}
6868+```
6969+7070+#### `fetchAllPublications`
7171+7272+```typescript
7373+async fetchAllPublications(
7474+ fetchFn?: typeof fetch
7575+): Promise<AtProtoRecord<Publication>[]>
7676+```
7777+7878+Fetches all publications for the configured DID with automatic pagination.
7979+8080+**Parameters:**
8181+8282+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
8383+8484+**Returns:** Promise resolving to an array of publication records
8585+8686+**Example:**
8787+8888+```typescript
8989+const publications = await client.fetchAllPublications(fetch);
9090+console.log(`Found ${publications.length} publications`);
9191+```
9292+9393+#### `fetchDocument`
9494+9595+```typescript
9696+async fetchDocument(
9797+ rkey: string,
9898+ fetchFn?: typeof fetch
9999+): Promise<AtProtoRecord<Document> | null>
100100+```
101101+102102+Fetches a single document by its record key.
103103+104104+**Parameters:**
105105+106106+- `rkey` (string): The record key (TID) of the document
107107+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
108108+109109+**Returns:** Promise resolving to the document record or null if not found
110110+111111+**Example:**
112112+113113+```typescript
114114+const doc = await client.fetchDocument('3lxbm5kqrs2s');
115115+if (doc) {
116116+ console.log(doc.value.title);
117117+}
118118+```
119119+120120+#### `fetchAllDocuments`
121121+122122+```typescript
123123+async fetchAllDocuments(
124124+ fetchFn?: typeof fetch
125125+): Promise<AtProtoRecord<Document>[]>
126126+```
127127+128128+Fetches all documents for the configured DID with automatic pagination. Results are sorted by `publishedAt` (newest first).
129129+130130+**Parameters:**
131131+132132+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
133133+134134+**Returns:** Promise resolving to an array of document records
135135+136136+**Example:**
137137+138138+```typescript
139139+const documents = await client.fetchAllDocuments(fetch);
140140+documents.forEach((doc) => {
141141+ console.log(doc.value.title, new Date(doc.value.publishedAt));
142142+});
143143+```
144144+145145+#### `fetchDocumentsByPublication`
146146+147147+```typescript
148148+async fetchDocumentsByPublication(
149149+ publicationUri: string,
150150+ fetchFn?: typeof fetch
151151+): Promise<AtProtoRecord<Document>[]>
152152+```
153153+154154+Fetches all documents that belong to a specific publication.
155155+156156+**Parameters:**
157157+158158+- `publicationUri` (string): AT URI of the publication
159159+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
160160+161161+**Returns:** Promise resolving to an array of document records
162162+163163+**Example:**
164164+165165+```typescript
166166+const docs = await client.fetchDocumentsByPublication(
167167+ 'at://did:plc:xxx/site.standard.publication/3lwafzkjqm25s'
168168+);
169169+```
170170+171171+#### `fetchByAtUri`
172172+173173+```typescript
174174+async fetchByAtUri<T = Publication | Document>(
175175+ atUri: string,
176176+ fetchFn?: typeof fetch
177177+): Promise<AtProtoRecord<T> | null>
178178+```
179179+180180+Fetches a record by its full AT URI.
181181+182182+**Parameters:**
183183+184184+- `atUri` (string): Full AT URI of the record
185185+- `fetchFn` (typeof fetch, optional): Custom fetch function for SSR
186186+187187+**Returns:** Promise resolving to the record or null if not found
188188+189189+**Example:**
190190+191191+```typescript
192192+const record = await client.fetchByAtUri(
193193+ 'at://did:plc:xxx/site.standard.publication/3lwafzkjqm25s'
194194+);
195195+```
196196+197197+#### `clearCache`
198198+199199+```typescript
200200+clearCache(): void
201201+```
202202+203203+Clears all cached data.
204204+205205+**Example:**
206206+207207+```typescript
208208+client.clearCache();
209209+```
210210+211211+#### `getPDS`
212212+213213+```typescript
214214+async getPDS(fetchFn?: typeof fetch): Promise<string>
215215+```
216216+217217+Gets the resolved PDS endpoint for the configured DID.
218218+219219+**Returns:** Promise resolving to the PDS URL
220220+221221+**Example:**
222222+223223+```typescript
224224+const pds = await client.getPDS();
225225+console.log('Using PDS:', pds);
226226+```
227227+228228+## Types
229229+230230+### SiteStandardConfig
231231+232232+Configuration object for the client.
233233+234234+```typescript
235235+interface SiteStandardConfig {
236236+ /** The DID to fetch records from */
237237+ did: string;
238238+ /** Optional custom PDS endpoint */
239239+ pds?: string;
240240+ /** Cache TTL in milliseconds (default: 5 minutes) */
241241+ cacheTTL?: number;
242242+}
243243+```
244244+245245+### Publication
246246+247247+Represents a site.standard.publication record.
248248+249249+```typescript
250250+interface Publication {
251251+ $type: 'site.standard.publication';
252252+ url: string;
253253+ name: string;
254254+ icon?: AtProtoBlob;
255255+ description?: string;
256256+ basicTheme?: BasicTheme;
257257+ preferences?: PublicationPreferences;
258258+}
259259+```
260260+261261+### Document
262262+263263+Represents a site.standard.document record.
264264+265265+```typescript
266266+interface Document {
267267+ $type: 'site.standard.document';
268268+ site: string; // AT URI or HTTPS URL
269269+ title: string;
270270+ path?: string;
271271+ description?: string;
272272+ coverImage?: AtProtoBlob;
273273+ content?: any;
274274+ textContent?: string;
275275+ bskyPostRef?: StrongRef;
276276+ tags?: string[];
277277+ publishedAt: string; // ISO 8601 datetime
278278+ updatedAt?: string; // ISO 8601 datetime
279279+}
280280+```
281281+282282+### AtProtoRecord
283283+284284+Generic wrapper for AT Protocol records.
285285+286286+```typescript
287287+interface AtProtoRecord<T> {
288288+ uri: string; // AT URI of the record
289289+ cid: string; // Content identifier
290290+ value: T; // The actual record value
291291+}
292292+```
293293+294294+### AtProtoBlob
295295+296296+Represents a blob (file) in AT Protocol.
297297+298298+```typescript
299299+interface AtProtoBlob {
300300+ $type: 'blob';
301301+ ref: {
302302+ $link: string; // CID or full URL after enhancement
303303+ };
304304+ mimeType: string;
305305+ size: number;
306306+}
307307+```
308308+309309+### BasicTheme
310310+311311+Theme configuration for a publication.
312312+313313+```typescript
314314+interface BasicTheme {
315315+ $type: 'site.standard.theme.basic';
316316+ accentColor?: string; // Hex color
317317+ backgroundColor?: string; // Hex color
318318+ textColor?: string; // Hex color
319319+}
320320+```
321321+322322+### StrongRef
323323+324324+Reference to another AT Protocol record.
325325+326326+```typescript
327327+interface StrongRef {
328328+ uri: string; // AT URI
329329+ cid: string; // Content identifier
330330+}
331331+```
332332+333333+### ResolvedIdentity
334334+335335+Result of DID resolution.
336336+337337+```typescript
338338+interface ResolvedIdentity {
339339+ did: string;
340340+ pds: string; // PDS endpoint URL
341341+ handle?: string; // User handle
342342+}
343343+```
344344+345345+## Utility Functions
346346+347347+### AT URI Utilities
348348+349349+#### `parseAtUri`
350350+351351+```typescript
352352+function parseAtUri(atUri: string): ParsedAtUri | null;
353353+```
354354+355355+Parses an AT URI into its components.
356356+357357+**Example:**
358358+359359+```typescript
360360+import { parseAtUri } from 'svelte-standard-site';
361361+362362+const parsed = parseAtUri('at://did:plc:xxx/site.standard.publication/rkey');
363363+// { did: 'did:plc:xxx', collection: 'site.standard.publication', rkey: 'rkey' }
364364+```
365365+366366+#### `atUriToHttps`
367367+368368+```typescript
369369+function atUriToHttps(atUri: string, pdsEndpoint: string): string | null;
370370+```
371371+372372+Converts an AT URI to an HTTPS URL for the getRecord XRPC endpoint.
373373+374374+**Example:**
375375+376376+```typescript
377377+import { atUriToHttps } from 'svelte-standard-site';
378378+379379+const url = atUriToHttps(
380380+ 'at://did:plc:xxx/site.standard.publication/rkey',
381381+ 'https://pds.example.com'
382382+);
383383+```
384384+385385+#### `buildAtUri`
386386+387387+```typescript
388388+function buildAtUri(did: string, collection: string, rkey: string): string;
389389+```
390390+391391+Constructs an AT URI from components.
392392+393393+**Example:**
394394+395395+```typescript
396396+import { buildAtUri } from 'svelte-standard-site';
397397+398398+const uri = buildAtUri('did:plc:xxx', 'site.standard.publication', 'rkey');
399399+// 'at://did:plc:xxx/site.standard.publication/rkey'
400400+```
401401+402402+#### `extractRkey`
403403+404404+```typescript
405405+function extractRkey(atUri: string): string | null;
406406+```
407407+408408+Extracts the record key from an AT URI.
409409+410410+**Example:**
411411+412412+```typescript
413413+import { extractRkey } from 'svelte-standard-site';
414414+415415+const rkey = extractRkey('at://did:plc:xxx/site.standard.publication/3lwafzkjqm25s');
416416+// '3lwafzkjqm25s'
417417+```
418418+419419+#### `isAtUri`
420420+421421+```typescript
422422+function isAtUri(uri: string): boolean;
423423+```
424424+425425+Validates if a string is a valid AT URI.
426426+427427+**Example:**
428428+429429+```typescript
430430+import { isAtUri } from 'svelte-standard-site';
431431+432432+console.log(isAtUri('at://did:plc:xxx/collection/rkey')); // true
433433+console.log(isAtUri('https://example.com')); // false
434434+```
435435+436436+### Agent Utilities
437437+438438+#### `resolveIdentity`
439439+440440+```typescript
441441+async function resolveIdentity(did: string, fetchFn?: typeof fetch): Promise<ResolvedIdentity>;
442442+```
443443+444444+Resolves a DID to its PDS endpoint using Slingshot.
445445+446446+**Example:**
447447+448448+```typescript
449449+import { resolveIdentity } from 'svelte-standard-site';
450450+451451+const identity = await resolveIdentity('did:plc:xxx');
452452+console.log(identity.pds); // 'https://...'
453453+```
454454+455455+#### `buildPdsBlobUrl`
456456+457457+```typescript
458458+function buildPdsBlobUrl(pds: string, did: string, cid: string): string;
459459+```
460460+461461+Constructs a blob URL for retrieving files from a PDS.
462462+463463+**Example:**
464464+465465+```typescript
466466+import { buildPdsBlobUrl } from 'svelte-standard-site';
467467+468468+const url = buildPdsBlobUrl('https://pds.example.com', 'did:plc:xxx', 'bafyrei...');
469469+```
470470+471471+### Cache
472472+473473+#### `cache`
474474+475475+Global cache instance used by the library.
476476+477477+```typescript
478478+const cache: Cache;
479479+```
480480+481481+**Methods:**
482482+483483+- `get<T>(key: string): T | null` - Get cached value
484484+- `set<T>(key: string, value: T, ttl?: number): void` - Set cached value
485485+- `delete(key: string): void` - Delete cached value
486486+- `clear(): void` - Clear all cached values
487487+- `setDefaultTTL(ttl: number): void` - Set default TTL
488488+489489+**Example:**
490490+491491+```typescript
492492+import { cache } from 'svelte-standard-site';
493493+494494+// Manual cache manipulation
495495+const data = cache.get('my-key');
496496+cache.set('my-key', { some: 'data' }, 60000);
497497+cache.clear();
498498+```
499499+500500+## Components
501501+502502+### PublicationCard
503503+504504+Displays a publication card with icon, name, description, and link.
505505+506506+**Props:**
507507+508508+```typescript
509509+interface Props {
510510+ publication: AtProtoRecord<Publication>;
511511+ class?: string;
512512+}
513513+```
514514+515515+**Example:**
516516+517517+```svelte
518518+<script>
519519+ import { PublicationCard } from 'svelte-standard-site';
520520+521521+ export let data;
522522+</script>
523523+524524+<PublicationCard publication={data.publication} class="my-custom-class" />
525525+```
526526+527527+**Styling:**
528528+529529+The component uses scoped CSS but exposes the following classes for customization:
530530+531531+- `.publication-card`
532532+- `.publication-header`
533533+- `.publication-icon`
534534+- `.publication-info`
535535+- `.publication-name`
536536+- `.publication-description`
537537+- `.publication-link`
538538+539539+### DocumentCard
540540+541541+Displays a document card with cover image, title, description, metadata, and tags.
542542+543543+**Props:**
544544+545545+```typescript
546546+interface Props {
547547+ document: AtProtoRecord<Document>;
548548+ class?: string;
549549+ showCover?: boolean;
550550+}
551551+```
552552+553553+**Example:**
554554+555555+```svelte
556556+<script>
557557+ import { DocumentCard } from 'svelte-standard-site';
558558+559559+ export let data;
560560+</script>
561561+562562+<DocumentCard document={data.document} showCover={true} class="my-custom-class" />
563563+```
564564+565565+**Styling:**
566566+567567+The component uses scoped CSS but exposes the following classes:
568568+569569+- `.document-card`
570570+- `.document-content`
571571+- `.document-cover`
572572+- `.document-body`
573573+- `.document-title`
574574+- `.document-description`
575575+- `.document-meta`
576576+- `.document-date`
577577+- `.document-updated`
578578+- `.document-tags`
579579+- `.tag`
580580+581581+## Configuration
582582+583583+### Environment Variables
584584+585585+#### `getConfigFromEnv`
586586+587587+```typescript
588588+function getConfigFromEnv(): SiteStandardConfig | null;
589589+```
590590+591591+Reads configuration from environment variables.
592592+593593+**Environment Variables:**
594594+595595+- `PUBLIC_ATPROTO_DID` (required): The DID to fetch records from
596596+- `PUBLIC_ATPROTO_PDS` (optional): Custom PDS endpoint
597597+- `PUBLIC_CACHE_TTL` (optional): Cache TTL in milliseconds
598598+599599+**Example:**
600600+601601+```typescript
602602+import { getConfigFromEnv } from 'svelte-standard-site/config/env';
603603+604604+const config = getConfigFromEnv();
605605+if (config) {
606606+ const client = createClient(config);
607607+}
608608+```
609609+610610+#### `validateEnv`
611611+612612+```typescript
613613+function validateEnv(): void;
614614+```
615615+616616+Validates that required environment variables are set. Throws an error if validation fails.
617617+618618+**Example:**
619619+620620+```typescript
621621+import { validateEnv } from 'svelte-standard-site/config/env';
622622+623623+try {
624624+ validateEnv();
625625+} catch (error) {
626626+ console.error('Missing required environment variables');
627627+}
628628+```
+203
CONTRIBUTING.md
···11+# Contributing to svelte-standard-site
22+33+Thank you for your interest in contributing! This document provides guidelines and information for contributors.
44+55+## Getting Started
66+77+### Prerequisites
88+99+- Node.js 18 or higher
1010+- pnpm 8 or higher
1111+1212+### Setup
1313+1414+1. Fork and clone the repository:
1515+1616+```bash
1717+git clone https://github.com/your-username/svelte-standard-site.git
1818+cd svelte-standard-site
1919+```
2020+2121+2. Install dependencies:
2222+2323+```bash
2424+pnpm install
2525+```
2626+2727+3. Create a `.env` file:
2828+2929+```bash
3030+cp .env.example .env
3131+# Edit .env and add your PUBLIC_ATPROTO_DID
3232+```
3333+3434+4. Start the development server:
3535+3636+```bash
3737+pnpm dev
3838+```
3939+4040+## Development Workflow
4141+4242+### Project Structure
4343+4444+```
4545+src/
4646+├── lib/ # Library source code
4747+│ ├── client.ts # Main client implementation
4848+│ ├── types.ts # TypeScript type definitions
4949+│ ├── index.ts # Public API exports
5050+│ ├── components/ # Reusable Svelte components
5151+│ │ ├── PublicationCard.svelte
5252+│ │ └── DocumentCard.svelte
5353+│ ├── config/ # Configuration utilities
5454+│ │ └── env.ts # Environment variable handling
5555+│ └── utils/ # Utility functions
5656+│ ├── agents.ts # AT Protocol agent utilities
5757+│ ├── at-uri.ts # AT URI parsing utilities
5858+│ └── cache.ts # Caching implementation
5959+└── routes/ # Demo/showcase pages
6060+ ├── +page.svelte
6161+ └── +page.server.ts
6262+```
6363+6464+### Commands
6565+6666+- `pnpm dev` - Start development server
6767+- `pnpm build` - Build the library
6868+- `pnpm check` - Run type checking
6969+- `pnpm format` - Format code with Prettier
7070+- `pnpm lint` - Check code formatting
7171+- `pnpm prepack` - Prepare package for publishing
7272+7373+## Making Changes
7474+7575+### Code Style
7676+7777+- We use Prettier for code formatting
7878+- Run `pnpm format` before committing
7979+- TypeScript strict mode is enabled
8080+- Follow the existing code structure and patterns
8181+8282+### Commit Messages
8383+8484+We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
8585+8686+- `feat:` - New features
8787+- `fix:` - Bug fixes
8888+- `docs:` - Documentation changes
8989+- `style:` - Code style changes (formatting, etc.)
9090+- `refactor:` - Code refactoring
9191+- `test:` - Test additions or changes
9292+- `chore:` - Build process or tooling changes
9393+9494+Example:
9595+9696+```
9797+feat: add support for custom PDS endpoints
9898+fix: resolve caching issue with blob URLs
9999+docs: update README with new examples
100100+```
101101+102102+### Pull Request Process
103103+104104+1. Create a new branch:
105105+106106+```bash
107107+git checkout -b feat/your-feature-name
108108+```
109109+110110+2. Make your changes and commit them:
111111+112112+```bash
113113+git add .
114114+git commit -m "feat: add your feature"
115115+```
116116+117117+3. Push to your fork:
118118+119119+```bash
120120+git push origin feat/your-feature-name
121121+```
122122+123123+4. Open a Pull Request on GitHub
124124+125125+5. Ensure:
126126+ - Code passes type checking (`pnpm check`)
127127+ - Code is properly formatted (`pnpm format`)
128128+ - Documentation is updated if needed
129129+ - Examples are added for new features
130130+131131+## What to Contribute
132132+133133+### Good First Issues
134134+135135+Look for issues labeled `good first issue` for beginner-friendly tasks.
136136+137137+### Areas for Contribution
138138+139139+- **Bug Fixes**: Report and fix bugs
140140+- **Features**: Implement new features (discuss in an issue first)
141141+- **Documentation**: Improve or expand documentation
142142+- **Examples**: Add new usage examples
143143+- **Components**: Create new reusable components
144144+- **Tests**: Add or improve test coverage
145145+- **Performance**: Optimize existing code
146146+147147+## Reporting Bugs
148148+149149+When reporting bugs, please include:
150150+151151+1. A clear description of the issue
152152+2. Steps to reproduce
153153+3. Expected behavior
154154+4. Actual behavior
155155+5. Environment details (Node version, OS, etc.)
156156+6. Code samples if applicable
157157+158158+## Feature Requests
159159+160160+For feature requests:
161161+162162+1. Check if the feature already exists or is planned
163163+2. Open an issue describing:
164164+ - The problem you're trying to solve
165165+ - Your proposed solution
166166+ - Any alternatives you've considered
167167+ - Examples of the desired behavior
168168+169169+## Code of Conduct
170170+171171+### Our Pledge
172172+173173+We are committed to providing a friendly, safe, and welcoming environment for all contributors.
174174+175175+### Expected Behavior
176176+177177+- Be respectful and inclusive
178178+- Welcome newcomers
179179+- Accept constructive criticism gracefully
180180+- Focus on what's best for the community
181181+- Show empathy towards others
182182+183183+### Unacceptable Behavior
184184+185185+- Harassment of any kind
186186+- Discriminatory language or actions
187187+- Personal attacks
188188+- Publishing others' private information
189189+- Other conduct which could reasonably be considered inappropriate
190190+191191+## Questions?
192192+193193+Feel free to:
194194+195195+- Open an issue for questions
196196+- Start a discussion in GitHub Discussions
197197+- Reach out to maintainers
198198+199199+## License
200200+201201+By contributing, you agree that your contributions will be licensed under the MIT License.
202202+203203+Thank you for contributing to svelte-standard-site! 🎉
+448
EXAMPLES.md
···11+# Usage Examples
22+33+This document provides comprehensive examples of using the svelte-standard-site library.
44+55+## Basic Setup
66+77+### 1. Install the Package
88+99+```bash
1010+pnpm add svelte-standard-site
1111+```
1212+1313+### 2. Configure Environment Variables
1414+1515+Create a `.env` file:
1616+1717+```env
1818+PUBLIC_ATPROTO_DID=did:plc:revjuqmkvrw6fnkxppqtszpv
1919+```
2020+2121+## Example 1: Simple Blog List
2222+2323+Fetch and display all documents as blog posts.
2424+2525+```typescript
2626+// src/routes/blog/+page.server.ts
2727+import { createClient } from 'svelte-standard-site';
2828+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
2929+import type { PageServerLoad } from './$types';
3030+3131+export const load: PageServerLoad = async ({ fetch }) => {
3232+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
3333+ const documents = await client.fetchAllDocuments(fetch);
3434+3535+ return {
3636+ posts: documents
3737+ };
3838+};
3939+```
4040+4141+```svelte
4242+<!-- src/routes/blog/+page.svelte -->
4343+<script lang="ts">
4444+ import { DocumentCard } from 'svelte-standard-site';
4545+ import type { PageData } from './$types';
4646+4747+ const { data }: { data: PageData } = $props();
4848+</script>
4949+5050+<div class="container">
5151+ <h1>Blog Posts</h1>
5252+ <div class="posts">
5353+ {#each data.posts as post}
5454+ <DocumentCard document={post} />
5555+ {/each}
5656+ </div>
5757+</div>
5858+```
5959+6060+## Example 2: Single Post Page
6161+6262+Display a single document by its record key (slug).
6363+6464+```typescript
6565+// src/routes/blog/[slug]/+page.server.ts
6666+import { createClient } from 'svelte-standard-site';
6767+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
6868+import { error } from '@sveltejs/kit';
6969+import type { PageServerLoad } from './$types';
7070+7171+export const load: PageServerLoad = async ({ params, fetch }) => {
7272+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
7373+ const document = await client.fetchDocument(params.slug, fetch);
7474+7575+ if (!document) {
7676+ throw error(404, 'Post not found');
7777+ }
7878+7979+ return {
8080+ post: document
8181+ };
8282+};
8383+```
8484+8585+```svelte
8686+<!-- src/routes/blog/[slug]/+page.svelte -->
8787+<script lang="ts">
8888+ import type { PageData } from './$types';
8989+9090+ const { data }: { data: PageData } = $props();
9191+ const post = data.post.value;
9292+</script>
9393+9494+<article>
9595+ {#if post.coverImage}
9696+ <img src={post.coverImage.ref.$link} alt={post.title} />
9797+ {/if}
9898+9999+ <h1>{post.title}</h1>
100100+101101+ <div class="meta">
102102+ <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
103103+ {#if post.tags}
104104+ <div class="tags">
105105+ {#each post.tags as tag}
106106+ <span class="tag">{tag}</span>
107107+ {/each}
108108+ </div>
109109+ {/if}
110110+ </div>
111111+112112+ {#if post.description}
113113+ <p class="description">{post.description}</p>
114114+ {/if}
115115+116116+ {#if post.textContent}
117117+ <div class="content">
118118+ {post.textContent}
119119+ </div>
120120+ {/if}
121121+</article>
122122+```
123123+124124+## Example 3: Publication-Filtered Posts
125125+126126+Display documents from a specific publication.
127127+128128+```typescript
129129+// src/routes/publications/[pubkey]/+page.server.ts
130130+import { createClient, buildAtUri } from 'svelte-standard-site';
131131+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
132132+import { error } from '@sveltejs/kit';
133133+import type { PageServerLoad } from './$types';
134134+135135+export const load: PageServerLoad = async ({ params, fetch }) => {
136136+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
137137+138138+ // Fetch the publication
139139+ const publication = await client.fetchPublication(params.pubkey, fetch);
140140+ if (!publication) {
141141+ throw error(404, 'Publication not found');
142142+ }
143143+144144+ // Fetch documents for this publication
145145+ const publicationUri = buildAtUri(PUBLIC_ATPROTO_DID, 'site.standard.publication', params.pubkey);
146146+ const documents = await client.fetchDocumentsByPublication(publicationUri, fetch);
147147+148148+ return {
149149+ publication,
150150+ documents
151151+ };
152152+};
153153+```
154154+155155+```svelte
156156+<!-- src/routes/publications/[pubkey]/+page.svelte -->
157157+<script lang="ts">
158158+ import { PublicationCard, DocumentCard } from 'svelte-standard-site';
159159+ import type { PageData } from './$types';
160160+161161+ const { data }: { data: PageData } = $props();
162162+</script>
163163+164164+<div class="publication-page">
165165+ <PublicationCard publication={data.publication} />
166166+167167+ <h2>Posts in this Publication</h2>
168168+ <div class="documents">
169169+ {#each data.documents as doc}
170170+ <DocumentCard document={doc} />
171171+ {/each}
172172+ </div>
173173+</div>
174174+```
175175+176176+## Example 4: Using AT URIs
177177+178178+Fetch a record using its full AT URI.
179179+180180+```typescript
181181+import { createClient } from 'svelte-standard-site';
182182+183183+const client = createClient({ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv' });
184184+185185+// Fetch using AT URI
186186+const publication = await client.fetchByAtUri(
187187+ 'at://did:plc:revjuqmkvrw6fnkxppqtszpv/site.standard.publication/3lwafzkjqm25s'
188188+);
189189+190190+console.log(publication?.value.name);
191191+```
192192+193193+## Example 5: Manual Configuration
194194+195195+Configure the client without environment variables.
196196+197197+```typescript
198198+import { createClient } from 'svelte-standard-site';
199199+200200+const client = createClient({
201201+ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv',
202202+ pds: 'https://cortinarius.us-west.host.bsky.network',
203203+ cacheTTL: 600000 // 10 minutes
204204+});
205205+```
206206+207207+## Example 6: Custom Publication Listing
208208+209209+Create a custom component to display publications.
210210+211211+```svelte
212212+<script lang="ts">
213213+ import type { AtProtoRecord, Publication } from 'svelte-standard-site';
214214+215215+ interface Props {
216216+ publications: AtProtoRecord<Publication>[];
217217+ }
218218+219219+ const { publications }: Props = $props();
220220+</script>
221221+222222+<div class="publications-grid">
223223+ {#each publications as pub}
224224+ <a href="/pub/{pub.uri.split('/').pop()}" class="pub-card">
225225+ {#if pub.value.icon}
226226+ <img src={pub.value.icon.ref.$link} alt={pub.value.name} />
227227+ {/if}
228228+ <h3>{pub.value.name}</h3>
229229+ <p>{pub.value.description}</p>
230230+ </a>
231231+ {/each}
232232+</div>
233233+234234+<style>
235235+ .publications-grid {
236236+ display: grid;
237237+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
238238+ gap: 1.5rem;
239239+ }
240240+241241+ .pub-card {
242242+ padding: 1.5rem;
243243+ border: 1px solid #e5e7eb;
244244+ border-radius: 0.5rem;
245245+ text-decoration: none;
246246+ color: inherit;
247247+ transition: all 0.2s;
248248+ }
249249+250250+ .pub-card:hover {
251251+ border-color: #3b82f6;
252252+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
253253+ }
254254+255255+ .pub-card img {
256256+ width: 100%;
257257+ height: 150px;
258258+ object-fit: cover;
259259+ border-radius: 0.375rem;
260260+ margin-bottom: 1rem;
261261+ }
262262+263263+ .pub-card h3 {
264264+ font-size: 1.25rem;
265265+ font-weight: 600;
266266+ margin-bottom: 0.5rem;
267267+ }
268268+269269+ .pub-card p {
270270+ font-size: 0.875rem;
271271+ color: #6b7280;
272272+ }
273273+</style>
274274+```
275275+276276+## Example 7: Search and Filter
277277+278278+Implement search and tag filtering.
279279+280280+```typescript
281281+// src/routes/search/+page.server.ts
282282+import { createClient } from 'svelte-standard-site';
283283+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
284284+import type { PageServerLoad } from './$types';
285285+286286+export const load: PageServerLoad = async ({ url, fetch }) => {
287287+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
288288+ const documents = await client.fetchAllDocuments(fetch);
289289+290290+ const query = url.searchParams.get('q')?.toLowerCase() || '';
291291+ const tag = url.searchParams.get('tag') || '';
292292+293293+ const filtered = documents.filter((doc) => {
294294+ const matchesQuery =
295295+ !query ||
296296+ doc.value.title.toLowerCase().includes(query) ||
297297+ doc.value.description?.toLowerCase().includes(query) ||
298298+ doc.value.textContent?.toLowerCase().includes(query);
299299+300300+ const matchesTag = !tag || doc.value.tags?.includes(tag);
301301+302302+ return matchesQuery && matchesTag;
303303+ });
304304+305305+ // Get all unique tags
306306+ const allTags = new Set<string>();
307307+ documents.forEach((doc) => {
308308+ doc.value.tags?.forEach((t) => allTags.add(t));
309309+ });
310310+311311+ return {
312312+ documents: filtered,
313313+ tags: Array.from(allTags).sort(),
314314+ query,
315315+ selectedTag: tag
316316+ };
317317+};
318318+```
319319+320320+## Example 8: RSS Feed
321321+322322+Generate an RSS feed from documents.
323323+324324+```typescript
325325+// src/routes/rss.xml/+server.ts
326326+import { createClient } from 'svelte-standard-site';
327327+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
328328+import type { RequestHandler } from './$types';
329329+330330+export const GET: RequestHandler = async ({ fetch }) => {
331331+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
332332+ const documents = await client.fetchAllDocuments(fetch);
333333+334334+ const rss = `<?xml version="1.0" encoding="UTF-8"?>
335335+<rss version="2.0">
336336+ <channel>
337337+ <title>My Blog</title>
338338+ <link>https://example.com</link>
339339+ <description>Blog posts from AT Protocol</description>
340340+ ${documents
341341+ .map(
342342+ (doc) => `
343343+ <item>
344344+ <title>${escapeXml(doc.value.title)}</title>
345345+ <description>${escapeXml(doc.value.description || '')}</description>
346346+ <pubDate>${new Date(doc.value.publishedAt).toUTCString()}</pubDate>
347347+ <guid>${doc.uri}</guid>
348348+ </item>`
349349+ )
350350+ .join('')}
351351+ </channel>
352352+</rss>`;
353353+354354+ return new Response(rss, {
355355+ headers: {
356356+ 'Content-Type': 'application/xml'
357357+ }
358358+ });
359359+};
360360+361361+function escapeXml(str: string): string {
362362+ return str
363363+ .replace(/&/g, '&')
364364+ .replace(/</g, '<')
365365+ .replace(/>/g, '>')
366366+ .replace(/"/g, '"')
367367+ .replace(/'/g, ''');
368368+}
369369+```
370370+371371+## Example 9: Sitemap Generation
372372+373373+Generate a sitemap from documents.
374374+375375+```typescript
376376+// src/routes/sitemap.xml/+server.ts
377377+import { createClient } from 'svelte-standard-site';
378378+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
379379+import type { RequestHandler } from './$types';
380380+381381+export const GET: RequestHandler = async ({ fetch }) => {
382382+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
383383+ const documents = await client.fetchAllDocuments(fetch);
384384+385385+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
386386+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
387387+ <url>
388388+ <loc>https://example.com</loc>
389389+ <changefreq>daily</changefreq>
390390+ <priority>1.0</priority>
391391+ </url>
392392+ ${documents
393393+ .map((doc) => {
394394+ const slug = doc.uri.split('/').pop();
395395+ return `
396396+ <url>
397397+ <loc>https://example.com/blog/${slug}</loc>
398398+ <lastmod>${new Date(doc.value.updatedAt || doc.value.publishedAt).toISOString()}</lastmod>
399399+ <changefreq>weekly</changefreq>
400400+ <priority>0.8</priority>
401401+ </url>`;
402402+ })
403403+ .join('')}
404404+</urlset>`;
405405+406406+ return new Response(sitemap, {
407407+ headers: {
408408+ 'Content-Type': 'application/xml'
409409+ }
410410+ });
411411+};
412412+```
413413+414414+## Example 10: Client-Side Usage
415415+416416+Use the client in browser context (not recommended for production due to CORS, but useful for prototyping).
417417+418418+```svelte
419419+<script lang="ts">
420420+ import { createClient } from 'svelte-standard-site';
421421+ import { onMount } from 'svelte';
422422+423423+ let documents = $state([]);
424424+ let loading = $state(true);
425425+426426+ onMount(async () => {
427427+ const client = createClient({ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv' });
428428+ documents = await client.fetchAllDocuments();
429429+ loading = false;
430430+ });
431431+</script>
432432+433433+{#if loading}
434434+ <p>Loading...</p>
435435+{:else}
436436+ {#each documents as doc}
437437+ <div>{doc.value.title}</div>
438438+ {/each}
439439+{/if}
440440+```
441441+442442+## Best Practices
443443+444444+1. **Always use SSR**: Fetch data in `+page.server.ts` or `+layout.server.ts` for better performance and SEO
445445+2. **Pass fetch function**: Always pass SvelteKit's `fetch` to client methods for proper SSR hydration
446446+3. **Handle errors**: Wrap client calls in try-catch and provide user-friendly error messages
447447+4. **Use caching**: The built-in cache reduces API calls, but you can adjust TTL as needed
448448+5. **Type safety**: Import and use TypeScript types for better developer experience
+303-31
README.md
···11-# Svelte library
11+# svelte-standard-site
2233-Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
33+A SvelteKit library for fetching and working with `site.standard.*` records from the AT Protocol.
4455-Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
55+## Features
6677-## Creating a project
77+- 🔄 **Automatic PDS Resolution** - Resolves DIDs to their Personal Data Server endpoints
88+- 📦 **Type-Safe** - Full TypeScript support with complete type definitions
99+- 🚀 **SSR Ready** - Works seamlessly with SvelteKit's server-side rendering
1010+- 💾 **Built-in Caching** - Reduces API calls with intelligent caching
1111+- 🎯 **Simple API** - Easy to use, set it and forget it configuration
1212+- 🔗 **AT URI Support** - Parse and convert AT URIs to HTTPS URLs
81399-If you're seeing this, you've probably already done this step. Congrats!
1414+## Installation
10151111-```sh
1212-# create a new project in the current directory
1313-npx sv create
1616+```bash
1717+pnpm add svelte-standard-site
1818+# or
1919+npm install svelte-standard-site
2020+# or
2121+yarn add svelte-standard-site
2222+```
14231515-# create a new project in my-app
1616-npx sv create my-app
2424+## Quick Start
2525+2626+### 1. Configure Environment Variables
2727+2828+Create a `.env` file in your project root:
2929+3030+```env
3131+PUBLIC_ATPROTO_DID=did:plc:your-did-here
3232+# Optional: specify a custom PDS endpoint
3333+PUBLIC_ATPROTO_PDS=https://your-pds.example.com
3434+# Optional: cache TTL in milliseconds (default: 300000 = 5 minutes)
3535+PUBLIC_CACHE_TTL=300000
1736```
18371919-## Developing
3838+### 2. Create a Client
20392121-Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
4040+```typescript
4141+import { createClient } from 'svelte-standard-site';
22422323-```sh
2424-npm run dev
4343+const client = createClient({
4444+ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv'
4545+});
4646+4747+// Fetch a single publication
4848+const publication = await client.fetchPublication('3lwafzkjqm25s');
4949+5050+// Fetch all publications
5151+const publications = await client.fetchAllPublications();
5252+5353+// Fetch all documents
5454+const documents = await client.fetchAllDocuments();
5555+5656+// Fetch documents for a specific publication
5757+const pubDocs = await client.fetchDocumentsByPublication(
5858+ 'at://did:plc:revjuqmkvrw6fnkxppqtszpv/site.standard.publication/3lwafzkjqm25s'
5959+);
6060+```
6161+6262+### 3. Use in SvelteKit Load Functions
6363+6464+```typescript
6565+// src/routes/+page.server.ts
6666+import { createClient } from 'svelte-standard-site';
6767+import { getConfigFromEnv } from 'svelte-standard-site/config/env';
6868+import type { PageServerLoad } from './$types';
6969+7070+export const load: PageServerLoad = async ({ fetch }) => {
7171+ const config = getConfigFromEnv();
7272+ if (!config) {
7373+ throw new Error('Missing configuration');
7474+ }
7575+7676+ const client = createClient(config);
7777+7878+ const [publications, documents] = await Promise.all([
7979+ client.fetchAllPublications(fetch),
8080+ client.fetchAllDocuments(fetch)
8181+ ]);
25822626-# or start the server and open the app in a new browser tab
2727-npm run dev -- --open
8383+ return {
8484+ publications,
8585+ documents
8686+ };
8787+};
2888```
29893030-Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
9090+```svelte
9191+<!-- src/routes/+page.svelte -->
9292+<script lang="ts">
9393+ import type { PageData } from './$types';
31943232-## Building
9595+ const { data }: { data: PageData } = $props();
9696+</script>
33973434-To build your library:
9898+<h1>Publications</h1>
9999+{#each data.publications as pub}
100100+ <article>
101101+ <h2>{pub.value.name}</h2>
102102+ <p>{pub.value.description}</p>
103103+ <a href={pub.value.url}>Visit</a>
104104+ </article>
105105+{/each}
351063636-```sh
3737-npm pack
107107+<h1>Documents</h1>
108108+{#each data.documents as doc}
109109+ <article>
110110+ <h2>{doc.value.title}</h2>
111111+ <p>{doc.value.description}</p>
112112+ <time>{new Date(doc.value.publishedAt).toLocaleDateString()}</time>
113113+ </article>
114114+{/each}
38115```
391164040-To create a production version of your showcase app:
117117+## API Reference
118118+119119+### `SiteStandardClient`
120120+121121+The main client for interacting with site.standard records.
122122+123123+#### Constructor
411244242-```sh
4343-npm run build
125125+```typescript
126126+new SiteStandardClient(config: SiteStandardConfig)
44127```
451284646-You can preview the production build with `npm run preview`.
129129+#### Methods
130130+131131+- `fetchPublication(rkey: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<Publication> | null>`
132132+ - Fetch a single publication by record key
133133+134134+- `fetchAllPublications(fetchFn?: typeof fetch): Promise<AtProtoRecord<Publication>[]>`
135135+ - Fetch all publications for the configured DID
136136+137137+- `fetchDocument(rkey: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<Document> | null>`
138138+ - Fetch a single document by record key
139139+140140+- `fetchAllDocuments(fetchFn?: typeof fetch): Promise<AtProtoRecord<Document>[]>`
141141+ - Fetch all documents for the configured DID, sorted by publishedAt (newest first)
142142+143143+- `fetchDocumentsByPublication(publicationUri: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<Document>[]>`
144144+ - Fetch all documents belonging to a specific publication
471454848-> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
146146+- `fetchByAtUri<T>(atUri: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<T> | null>`
147147+ - Fetch any record by its AT URI
491485050-## Publishing
149149+- `clearCache(): void`
150150+ - Clear all cached data
511515252-Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
152152+- `getPDS(fetchFn?: typeof fetch): Promise<string>`
153153+ - Get the resolved PDS endpoint
531545454-To publish your library to [npm](https://www.npmjs.com):
155155+### Types
551565656-```sh
5757-npm publish
157157+```typescript
158158+interface Publication {
159159+ $type: 'site.standard.publication';
160160+ url: string;
161161+ name: string;
162162+ icon?: AtProtoBlob;
163163+ description?: string;
164164+ basicTheme?: BasicTheme;
165165+ preferences?: PublicationPreferences;
166166+}
167167+168168+interface Document {
169169+ $type: 'site.standard.document';
170170+ site: string; // AT URI or HTTPS URL
171171+ title: string;
172172+ path?: string;
173173+ description?: string;
174174+ coverImage?: AtProtoBlob;
175175+ content?: any;
176176+ textContent?: string;
177177+ bskyPostRef?: StrongRef;
178178+ tags?: string[];
179179+ publishedAt: string;
180180+ updatedAt?: string;
181181+}
182182+183183+interface AtProtoRecord<T> {
184184+ uri: string;
185185+ cid: string;
186186+ value: T;
187187+}
58188```
189189+190190+### Utility Functions
191191+192192+#### AT URI Utilities
193193+194194+```typescript
195195+import { parseAtUri, atUriToHttps, buildAtUri, isAtUri } from 'svelte-standard-site';
196196+197197+// Parse an AT URI
198198+const parsed = parseAtUri('at://did:plc:xxx/site.standard.publication/rkey');
199199+// Returns: { did: 'did:plc:xxx', collection: 'site.standard.publication', rkey: 'rkey' }
200200+201201+// Convert AT URI to HTTPS URL
202202+const url = atUriToHttps(
203203+ 'at://did:plc:xxx/site.standard.publication/rkey',
204204+ 'https://pds.example.com'
205205+);
206206+// Returns: 'https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=...'
207207+208208+// Build an AT URI
209209+const uri = buildAtUri('did:plc:xxx', 'site.standard.publication', 'rkey');
210210+// Returns: 'at://did:plc:xxx/site.standard.publication/rkey'
211211+212212+// Validate AT URI
213213+const valid = isAtUri('at://did:plc:xxx/site.standard.publication/rkey');
214214+// Returns: true
215215+```
216216+217217+#### PDS Resolution
218218+219219+```typescript
220220+import { resolveIdentity, buildPdsBlobUrl } from 'svelte-standard-site';
221221+222222+// Resolve a DID to its PDS
223223+const identity = await resolveIdentity('did:plc:xxx');
224224+// Returns: { did: 'did:plc:xxx', pds: 'https://...', handle?: 'user.bsky.social' }
225225+226226+// Build a blob URL
227227+const blobUrl = buildPdsBlobUrl('https://pds.example.com', 'did:plc:xxx', 'bafyrei...');
228228+// Returns: 'https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=...&cid=...'
229229+```
230230+231231+## Configuration
232232+233233+### From Environment Variables
234234+235235+```typescript
236236+import { getConfigFromEnv, validateEnv } from 'svelte-standard-site/config/env';
237237+238238+// Get config (returns null if missing)
239239+const config = getConfigFromEnv();
240240+241241+// Validate config (throws if missing)
242242+validateEnv();
243243+```
244244+245245+### Manual Configuration
246246+247247+```typescript
248248+import { createClient } from 'svelte-standard-site';
249249+250250+const client = createClient({
251251+ did: 'did:plc:revjuqmkvrw6fnkxppqtszpv',
252252+ pds: 'https://cortinarius.us-west.host.bsky.network', // optional
253253+ cacheTTL: 300000 // optional, in milliseconds
254254+});
255255+```
256256+257257+## Caching
258258+259259+The library includes built-in caching to reduce API calls:
260260+261261+- Default TTL: 5 minutes (300,000ms)
262262+- Configurable via `cacheTTL` option or `PUBLIC_CACHE_TTL` env var
263263+- Cache can be cleared manually with `client.clearCache()`
264264+265265+## AT URI Structure
266266+267267+AT URIs follow this format:
268268+269269+```
270270+at://did:plc:revjuqmkvrw6fnkxppqtszpv/site.standard.publication/3lwafzkjqm25s
271271+ └─────────┬─────────┘ └──────────┬──────────┘ └────┬────┘
272272+ DID Collection Record Key
273273+```
274274+275275+The library automatically converts these to HTTPS URLs for API calls:
276276+277277+```
278278+https://cortinarius.us-west.host.bsky.network/xrpc/com.atproto.repo.getRecord?repo=did:plc:revjuqmkvrw6fnkxppqtszpv&collection=site.standard.publication&rkey=3lwafzkjqm25s
279279+```
280280+281281+## Example: Building a Blog
282282+283283+```typescript
284284+// src/routes/blog/+page.server.ts
285285+import { createClient } from 'svelte-standard-site';
286286+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
287287+288288+export const load = async ({ fetch }) => {
289289+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
290290+ const documents = await client.fetchAllDocuments(fetch);
291291+292292+ return {
293293+ posts: documents.map((doc) => ({
294294+ title: doc.value.title,
295295+ description: doc.value.description,
296296+ publishedAt: doc.value.publishedAt,
297297+ slug: doc.uri.split('/').pop(),
298298+ tags: doc.value.tags || []
299299+ }))
300300+ };
301301+};
302302+```
303303+304304+```typescript
305305+// src/routes/blog/[slug]/+page.server.ts
306306+import { createClient } from 'svelte-standard-site';
307307+import { PUBLIC_ATPROTO_DID } from '$env/static/public';
308308+import { error } from '@sveltejs/kit';
309309+310310+export const load = async ({ params, fetch }) => {
311311+ const client = createClient({ did: PUBLIC_ATPROTO_DID });
312312+ const document = await client.fetchDocument(params.slug, fetch);
313313+314314+ if (!document) {
315315+ throw error(404, 'Post not found');
316316+ }
317317+318318+ return {
319319+ post: document.value
320320+ };
321321+};
322322+```
323323+324324+## License
325325+326326+MIT
327327+328328+## Contributing
329329+330330+Contributions are welcome! Please feel free to submit a Pull Request.
···11+import { AtpAgent } from '@atproto/api';
22+import type { ResolvedIdentity } from '../types.js';
33+import { cache } from './cache.js';
44+55+/**
66+ * Creates an AtpAgent with optional fetch function injection
77+ */
88+export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent {
99+ const wrappedFetch = fetchFn
1010+ ? async (url: URL | RequestInfo, init?: RequestInit) => {
1111+ const urlStr = url instanceof URL ? url.toString() : url;
1212+ const response = await fetchFn(urlStr, init);
1313+1414+ const headers = new Headers(response.headers);
1515+ if (!headers.has('content-type')) {
1616+ headers.set('content-type', 'application/json');
1717+ }
1818+1919+ return new Response(response.body, {
2020+ status: response.status,
2121+ statusText: response.statusText,
2222+ headers
2323+ });
2424+ }
2525+ : undefined;
2626+2727+ return new AtpAgent({
2828+ service,
2929+ ...(wrappedFetch && { fetch: wrappedFetch })
3030+ });
3131+}
3232+3333+/**
3434+ * Resolves a DID to find its PDS endpoint using Slingshot
3535+ * @param did - DID to resolve
3636+ * @param fetchFn - Optional fetch function for SSR
3737+ * @returns Resolved identity with PDS endpoint
3838+ */
3939+export async function resolveIdentity(
4040+ did: string,
4141+ fetchFn?: typeof fetch
4242+): Promise<ResolvedIdentity> {
4343+ const cacheKey = `identity:${did}`;
4444+ const cached = cache.get<ResolvedIdentity>(cacheKey);
4545+ if (cached) return cached;
4646+4747+ const _fetch = fetchFn ?? globalThis.fetch;
4848+4949+ const response = await _fetch(
5050+ `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`
5151+ );
5252+5353+ if (!response.ok) {
5454+ throw new Error(`Failed to resolve DID: ${response.status} ${response.statusText}`);
5555+ }
5656+5757+ const rawText = await response.text();
5858+ const data = JSON.parse(rawText);
5959+6060+ if (!data.did || !data.pds) {
6161+ throw new Error('Invalid response from identity resolver');
6262+ }
6363+6464+ cache.set(cacheKey, data);
6565+ return data;
6666+}
6767+6868+/**
6969+ * Gets or creates a PDS-specific agent
7070+ * @param did - DID to resolve PDS for
7171+ * @param fetchFn - Optional fetch function for SSR
7272+ * @returns AtpAgent configured for the user's PDS
7373+ */
7474+export async function getPDSAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> {
7575+ const resolved = await resolveIdentity(did, fetchFn);
7676+ return createAgent(resolved.pds, fetchFn);
7777+}
7878+7979+/**
8080+ * Executes a function with automatic fallback
8181+ * @param did - The DID to resolve
8282+ * @param operation - The operation to execute
8383+ * @param fetchFn - Optional fetch function for SSR
8484+ */
8585+export async function withFallback<T>(
8686+ did: string,
8787+ operation: (agent: AtpAgent) => Promise<T>,
8888+ fetchFn?: typeof fetch
8989+): Promise<T> {
9090+ const agents = [
9191+ () => getPDSAgent(did, fetchFn),
9292+ () =>
9393+ Promise.resolve(
9494+ fetchFn
9595+ ? createAgent('https://public.api.bsky.app', fetchFn)
9696+ : createAgent('https://public.api.bsky.app')
9797+ )
9898+ ];
9999+100100+ let lastError: any;
101101+102102+ for (const getAgent of agents) {
103103+ try {
104104+ const agent = await getAgent();
105105+ return await operation(agent);
106106+ } catch (error) {
107107+ lastError = error;
108108+ }
109109+ }
110110+111111+ throw lastError;
112112+}
113113+114114+/**
115115+ * Build a PDS blob URL
116116+ * @param pds - PDS endpoint
117117+ * @param did - Repository DID
118118+ * @param cid - Blob CID
119119+ * @returns Full blob URL
120120+ */
121121+export function buildPdsBlobUrl(pds: string, did: string, cid: string): string {
122122+ const pdsBase = pds.replace(/\/$/, '');
123123+ return `${pdsBase}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
124124+}
+89
src/lib/utils/at-uri.ts
···11+/**
22+ * AT URI parsing and conversion utilities
33+ */
44+55+/**
66+ * Parsed AT URI components
77+ */
88+export interface ParsedAtUri {
99+ did: string;
1010+ collection: string;
1111+ rkey: string;
1212+}
1313+1414+/**
1515+ * Parse an AT URI into its components
1616+ * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey
1717+ * @returns Parsed components or null if invalid
1818+ */
1919+export function parseAtUri(atUri: string): ParsedAtUri | null {
2020+ if (!atUri.startsWith('at://')) return null;
2121+2222+ const withoutProtocol = atUri.slice(5); // Remove "at://"
2323+ const parts = withoutProtocol.split('/');
2424+2525+ if (parts.length !== 3) return null;
2626+2727+ const [did, collection, rkey] = parts;
2828+2929+ if (!did.startsWith('did:') || !collection || !rkey) return null;
3030+3131+ return { did, collection, rkey };
3232+}
3333+3434+/**
3535+ * Convert AT URI to HTTPS URL for com.atproto.repo.getRecord
3636+ * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey
3737+ * @param pdsEndpoint - PDS endpoint (e.g., "https://cortinarius.us-west.host.bsky.network")
3838+ * @returns HTTPS URL for getRecord XRPC call
3939+ */
4040+export function atUriToHttps(atUri: string, pdsEndpoint: string): string | null {
4141+ const parsed = parseAtUri(atUri);
4242+ if (!parsed) return null;
4343+4444+ // Ensure PDS endpoint doesn't have trailing slash
4545+ const pds = pdsEndpoint.replace(/\/$/, '');
4646+4747+ return `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parsed.did)}&collection=${encodeURIComponent(parsed.collection)}&rkey=${encodeURIComponent(parsed.rkey)}`;
4848+}
4949+5050+/**
5151+ * Construct an AT URI from components
5252+ * @param did - DID of the repository
5353+ * @param collection - Collection name
5454+ * @param rkey - Record key
5555+ * @returns AT URI string
5656+ */
5757+export function buildAtUri(did: string, collection: string, rkey: string): string {
5858+ return `at://${did}/${collection}/${rkey}`;
5959+}
6060+6161+/**
6262+ * Extract rkey from an AT URI
6363+ * @param atUri - AT URI
6464+ * @returns rkey or null if invalid
6565+ */
6666+export function extractRkey(atUri: string): string | null {
6767+ const parsed = parseAtUri(atUri);
6868+ return parsed?.rkey ?? null;
6969+}
7070+7171+/**
7272+ * Validate if string is a valid AT URI
7373+ * @param uri - String to validate
7474+ * @returns true if valid AT URI
7575+ */
7676+export function isAtUri(uri: string): boolean {
7777+ return parseAtUri(uri) !== null;
7878+}
7979+8080+/**
8181+ * Convert DID to PDS hostname format
8282+ * @param did - DID to convert
8383+ * @returns PDS hostname or null if unable to derive
8484+ */
8585+export function didToPdsHostname(did: string): string | null {
8686+ // This is a simplified version - real implementation should use DID resolution
8787+ // For now, we'll return null and rely on explicit PDS configuration
8888+ return null;
8989+}