my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Standard.site lexicons integration (v10.6.0)

Add support for Standard.site publications and documents alongside Leaflet
and WhiteWind. Includes platform-aware routing, theme support, and unified
blog feed integration.

- New: fetchStandardSitePublications() and fetchStandardSiteDocuments()
- New: Platform-aware slug mappings with 'leaflet' | 'standard.site'
- New: Comprehensive documentation and migration guides
- Enhanced: BlogPost type includes 'standard.site' platform
- Enhanced: Route handlers support Standard.site redirects

Fully backwards compatible - no breaking changes.
See docs/standard-site-integration.md for details.

+1458 -69
+163
docs/INTEGRATION_SUMMARY.md
··· 1 + # Standard.site Lexicons Integration Summary 2 + 3 + ## What Was Done 4 + 5 + Successfully integrated [Standard.site](https://standard.site/) lexicons into the website alongside existing Leaflet and WhiteWind support. The integration uses the lexicons as a reference (from `/Volumes/Storage/Developer/clones/standard-site-lexicons`) to fetch and display Standard.site publications and documents. 6 + 7 + ## Changes Made 8 + 9 + ### 1. Type Definitions (`/src/lib/services/atproto/types.ts`) 10 + Added comprehensive TypeScript types for Standard.site: 11 + - `StandardSiteThemeColor` - RGB/RGBA color values 12 + - `StandardSiteBasicTheme` - Theme with background, foreground, accent colors 13 + - `StandardSitePublication` - Publication metadata 14 + - `StandardSitePublicationsData` - Collection of publications 15 + - `StandardSiteDocument` - Document metadata and content 16 + - `StandardSiteDocumentsData` - Collection of documents 17 + - Updated `BlogPost` platform type to include `'standard.site'` 18 + 19 + ### 2. Fetch Service (`/src/lib/services/atproto/standard.ts`) 20 + Created new service module with two main functions: 21 + 22 + **`fetchStandardSitePublications()`** 23 + - Fetches all `site.standard.publication` records 24 + - Resolves publication icons from AT Protocol blobs 25 + - Extracts theme colors and preferences 26 + - Caches results with key `standard-site:publications:{DID}` 27 + 28 + **`fetchStandardSiteDocuments()`** 29 + - Fetches all `site.standard.document` records 30 + - Maps documents to their publications 31 + - Resolves cover images from blobs 32 + - Builds canonical URLs based on publication URL + document path 33 + - Handles both publication-based and loose documents 34 + - Sorts by `publishedAt` (newest first) 35 + - Caches results with key `standard-site:documents:{DID}` 36 + 37 + ### 3. Blog Feed Integration (`/src/lib/services/atproto/posts.ts`) 38 + Updated `fetchBlogPosts()` to include Standard.site documents: 39 + - Fetches documents from Standard.site alongside WhiteWind and Leaflet 40 + - Adds documents to unified feed with platform `'standard.site'` 41 + - Preserves existing sorting and top 5 limitation 42 + 43 + ### 4. Exports (`/src/lib/services/atproto/index.ts`) 44 + Exported new types and functions: 45 + - All Standard.site types 46 + - `fetchStandardSitePublications` 47 + - `fetchStandardSiteDocuments` 48 + 49 + ### 5. Slug Mappings (`/src/lib/data/slug-mappings.ts`) 50 + Enhanced slug mapping configuration: 51 + - Added `PublicationPlatform` type: `'leaflet' | 'standard.site'` 52 + - Extended `SlugMapping` interface with optional `platform` field 53 + - Updated all existing mappings to explicitly use `platform: 'leaflet'` 54 + - Added examples for Standard.site mappings in comments 55 + - Defaults to `'leaflet'` for backwards compatibility 56 + 57 + ### 6. Slug Configuration (`/src/lib/config/slugs.ts`) 58 + Added platform-aware slug resolution: 59 + - New `getPublicationFromSlug()` returns `{ rkey, platform }` 60 + - Maintained `getPublicationRkeyFromSlug()` for backwards compatibility 61 + - Imports `PublicationPlatform` type 62 + 63 + ### 7. Slug Route Handler (`/src/routes/[slug=slug]/+server.ts`) 64 + Updated to support both platforms: 65 + - Uses `getPublicationFromSlug()` instead of `getPublicationRkeyFromSlug()` 66 + - Branches logic based on platform 67 + - For Standard.site: Uses publication URL directly 68 + - For Leaflet: Uses base_path or /lish format 69 + - Updated error messages to reference correct config file 70 + 71 + ### 8. Document Route Handler (`/src/routes/[slug=slug]/[rkey]/+server.ts`) 72 + Enhanced document detection and routing: 73 + - Updated `detectPostPlatform()` to accept and prioritize `platform` parameter 74 + - Added Standard.site document detection via `site.standard.document` collection 75 + - Verifies document belongs to requested publication 76 + - Builds URLs using publication URL + document path 77 + - Falls back to Leaflet and WhiteWind as needed 78 + - Updated error messages with platform-specific information 79 + 80 + ### 9. Documentation (`/docs/standard-site-integration.md`) 81 + Created comprehensive documentation covering: 82 + - Overview of Standard.site lexicons 83 + - Supported lexicon collections and their fields 84 + - File structure and changes 85 + - Usage examples and code snippets 86 + - Type definitions 87 + - URL resolution patterns 88 + - Caching strategy 89 + - Integration with blog feed 90 + - Component examples 91 + - Troubleshooting guide 92 + 93 + ## Lexicon Collections Supported 94 + 95 + ### `site.standard.publication` 96 + - Fetched from AT Protocol records 97 + - Provides publication metadata, theme, and URL 98 + - Used for slug-based routing 99 + 100 + ### `site.standard.document` 101 + - Fetched from AT Protocol records 102 + - Contains full document metadata 103 + - Supports both publication-based and standalone documents 104 + - Integrated into unified blog feed 105 + 106 + ## URL Patterns 107 + 108 + ### Publication Redirects 109 + - `/{slug}` → `{publication.url}` 110 + 111 + ### Document Redirects 112 + - `/{slug}/{rkey}` → `{publication.url}{document.path}` 113 + 114 + ## Backwards Compatibility 115 + 116 + All changes maintain full backwards compatibility: 117 + - Existing Leaflet slugs work without modification 118 + - `getPublicationRkeyFromSlug()` still works 119 + - Default platform is `'leaflet'` if not specified 120 + - WhiteWind integration unchanged 121 + 122 + ## Testing Recommendations 123 + 124 + 1. **Slug Mappings**: Add a Standard.site publication to `slug-mappings.ts` 125 + 2. **Publications**: Test fetching via `fetchStandardSitePublications()` 126 + 3. **Documents**: Test fetching via `fetchStandardSiteDocuments()` 127 + 4. **Blog Feed**: Verify Standard.site docs appear in `fetchBlogPosts()` 128 + 5. **Routing**: Test `/{slug}` and `/{slug}/{rkey}` redirects 129 + 6. **Themes**: Verify theme colors are correctly extracted 130 + 7. **Images**: Check icon and cover image blob resolution 131 + 132 + ## Next Steps 133 + 134 + To use this integration: 135 + 136 + 1. Create `site.standard.publication` records in your AT Protocol repository 137 + 2. Create `site.standard.document` records linked to your publications 138 + 3. Add slug mappings with `platform: 'standard.site'` 139 + 4. Access content via slug routes (e.g., `/myblog`, `/myblog/3labc123`) 140 + 141 + ## Reference 142 + 143 + The lexicons were integrated based on the schema definitions in: 144 + - `/Volumes/Storage/Developer/clones/standard-site-lexicons/src/lexicons/` 145 + 146 + Key lexicon files referenced: 147 + - `site.standard.publication.ts` 148 + - `site.standard.document.ts` 149 + - `site.standard.theme.basic.ts` 150 + - `site.standard.theme.color.ts` 151 + - `site.standard.graph.subscription.ts` (not yet integrated) 152 + 153 + ## Architecture 154 + 155 + The integration follows the same patterns as Leaflet: 156 + - Fetch functions in dedicated service module 157 + - Type definitions in shared types file 158 + - Caching via existing cache infrastructure 159 + - Slug-based routing via slug configuration 160 + - Platform detection in route handlers 161 + - Unified blog feed integration 162 + 163 + This ensures consistency with the existing codebase and makes it easy to maintain both platforms side-by-side.
+311
docs/MIGRATION_GUIDE.md
··· 1 + # Migrating from Leaflet to Standard.site 2 + 3 + This guide helps you migrate content from Leaflet to Standard.site or run both platforms side-by-side. 4 + 5 + ## Should You Migrate? 6 + 7 + **Consider Standard.site if you want:** 8 + - More flexible content models (open union for content types) 9 + - Direct theme integration (colors defined in publication) 10 + - Better document organization (tags, paths, cover images) 11 + - Stronger ties to your own domain (site URL is part of publication) 12 + - Bluesky integration (post references for comments) 13 + 14 + **Stay with Leaflet if you prefer:** 15 + - The Leaflet ecosystem and community 16 + - The /lish hosted format 17 + - Simpler publication structure 18 + 19 + **You can use both!** The integration supports running Leaflet and Standard.site simultaneously. 20 + 21 + ## Side-by-Side Setup (Recommended) 22 + 23 + The easiest approach is to use both platforms together: 24 + 25 + ### 1. Keep Existing Leaflet Setup 26 + Your existing Leaflet publications continue to work: 27 + 28 + ```typescript 29 + // src/lib/data/slug-mappings.ts 30 + export const slugMappings: SlugMapping[] = [ 31 + { 32 + slug: 'blog', 33 + publicationRkey: '3m3x4bgbsh22k', 34 + platform: 'leaflet' // Existing Leaflet blog 35 + }, 36 + // ... other Leaflet mappings 37 + ]; 38 + ``` 39 + 40 + ### 2. Add Standard.site Publications 41 + Add new Standard.site publications alongside: 42 + 43 + ```typescript 44 + export const slugMappings: SlugMapping[] = [ 45 + // Existing Leaflet 46 + { 47 + slug: 'blog', 48 + publicationRkey: '3m3x4bgbsh22k', 49 + platform: 'leaflet' 50 + }, 51 + // New Standard.site 52 + { 53 + slug: 'articles', 54 + publicationRkey: '3labc123xyz', 55 + platform: 'standard.site' 56 + } 57 + ]; 58 + ``` 59 + 60 + ### 3. Content Appears in Unified Feed 61 + Both platforms' content automatically appears in `fetchBlogPosts()`: 62 + 63 + ```typescript 64 + const { posts } = await fetchBlogPosts(); 65 + // Returns posts from both Leaflet and Standard.site 66 + ``` 67 + 68 + ## Full Migration Path 69 + 70 + If you want to completely switch from Leaflet to Standard.site: 71 + 72 + ### Step 1: Create Standard.site Publication 73 + 74 + Create a `site.standard.publication` record in your AT Protocol repository: 75 + 76 + ```json 77 + { 78 + "$type": "site.standard.publication", 79 + "name": "My Blog", 80 + "url": "https://myblog.com", 81 + "description": "My personal blog about technology", 82 + "icon": { /* blob reference */ }, 83 + "basicTheme": { 84 + "background": { "r": 255, "g": 255, "b": 255 }, 85 + "foreground": { "r": 0, "g": 0, "b": 0 }, 86 + "accent": { "r": 59, "g": 130, "b": 246 }, 87 + "accentForeground": { "r": 255, "g": 255, "b": 255 } 88 + }, 89 + "preferences": { 90 + "showInDiscover": true 91 + } 92 + } 93 + ``` 94 + 95 + ### Step 2: Migrate Documents 96 + 97 + For each Leaflet document, create a corresponding Standard.site document: 98 + 99 + **Leaflet Document:** 100 + ```json 101 + { 102 + "$type": "pub.leaflet.document", 103 + "title": "My Post", 104 + "publication": "at://did:plc:abc/pub.leaflet.publication/xyz", 105 + "content": { /* markdown or HTML */ }, 106 + "createdAt": "2024-01-01T12:00:00Z" 107 + } 108 + ``` 109 + 110 + **Standard.site Document:** 111 + ```json 112 + { 113 + "$type": "site.standard.document", 114 + "title": "My Post", 115 + "site": "at://did:plc:abc/site.standard.publication/xyz", 116 + "path": "/my-post", 117 + "description": "A brief description of my post", 118 + "content": { /* open union - flexible format */ }, 119 + "textContent": "Plain text version...", 120 + "tags": ["technology", "tutorial"], 121 + "publishedAt": "2024-01-01T12:00:00Z", 122 + "updatedAt": "2024-01-15T14:30:00Z" 123 + } 124 + ``` 125 + 126 + ### Step 3: Update Slug Mapping 127 + 128 + Change the platform in your slug mapping: 129 + 130 + ```typescript 131 + // Before 132 + { 133 + slug: 'blog', 134 + publicationRkey: 'leaflet-rkey', 135 + platform: 'leaflet' 136 + } 137 + 138 + // After 139 + { 140 + slug: 'blog', 141 + publicationRkey: 'standard-site-rkey', 142 + platform: 'standard.site' 143 + } 144 + ``` 145 + 146 + ### Step 4: Test Migration 147 + 148 + 1. **Publications**: Verify publications appear via `fetchStandardSitePublications()` 149 + 2. **Documents**: Check documents via `fetchStandardSiteDocuments()` 150 + 3. **URLs**: Test redirects at `/{slug}` and `/{slug}/{rkey}` 151 + 4. **Blog Feed**: Confirm posts appear in `fetchBlogPosts()` 152 + 153 + ### Step 5: Archive Leaflet Content (Optional) 154 + 155 + You can keep Leaflet records for historical purposes or delete them: 156 + 157 + - **Keep**: Records remain accessible via AT Protocol 158 + - **Delete**: Use AT Protocol tools to delete records (careful - permanent!) 159 + 160 + ## Field Mapping Reference 161 + 162 + | Leaflet Field | Standard.site Field | Notes | 163 + |---------------|---------------------|-------| 164 + | `title` | `title` | Direct mapping | 165 + | `publication` | `site` | Both use AT URI reference | 166 + | `content` | `content` | Standard.site uses open union | 167 + | - | `textContent` | New: plaintext version | 168 + | - | `description` | New: excerpt/summary | 169 + | - | `path` | New: document URL path | 170 + | - | `coverImage` | New: cover/thumbnail | 171 + | `createdAt` | `publishedAt` | Rename for clarity | 172 + | - | `updatedAt` | New: track edits | 173 + | - | `tags` | New: categorization | 174 + | - | `bskyPostRef` | New: link to Bluesky post | 175 + 176 + ## Theme Migration 177 + 178 + Leaflet doesn't have built-in themes, but Standard.site does: 179 + 180 + ```typescript 181 + // Define your theme in the publication 182 + const theme = { 183 + background: { r: 255, g: 255, b: 255 }, // White 184 + foreground: { r: 17, g: 24, b: 39 }, // Dark gray 185 + accent: { r: 59, g: 130, b: 246 }, // Blue 186 + accentForeground: { r: 255, g: 255, b: 255 } // White 187 + }; 188 + 189 + // Use in your site 190 + const bgColor = `rgb(${theme.background.r}, ${theme.background.g}, ${theme.background.b})`; 191 + ``` 192 + 193 + ## URL Structure Comparison 194 + 195 + ### Leaflet URLs 196 + 197 + **Publication:** 198 + - With base_path: `https://myblog.com` 199 + - Without: `https://leaflet.pub/lish/{DID}/{publicationRkey}` 200 + 201 + **Document:** 202 + - With base_path: `https://myblog.com/{rkey}` 203 + - Without: `https://leaflet.pub/lish/{DID}/{publicationRkey}/{rkey}` 204 + 205 + ### Standard.site URLs 206 + 207 + **Publication:** 208 + - `{publication.url}` (e.g., `https://myblog.com`) 209 + 210 + **Document:** 211 + - `{publication.url}{document.path}` (e.g., `https://myblog.com/my-post`) 212 + 213 + ## Migration Tools 214 + 215 + ### Automated Migration Script (Example) 216 + 217 + ```typescript 218 + // migrate-to-standard-site.ts 219 + import { fetchLeafletPublications, fetchStandardSitePublications } from '$lib/services/atproto'; 220 + 221 + async function migratePublication(leafletRkey: string) { 222 + // 1. Fetch Leaflet publication 223 + const { publications: leafletPubs } = await fetchLeafletPublications(); 224 + const leafletPub = leafletPubs.find(p => p.rkey === leafletRkey); 225 + 226 + if (!leafletPub) { 227 + throw new Error('Leaflet publication not found'); 228 + } 229 + 230 + // 2. Create Standard.site publication 231 + const standardPub = { 232 + $type: 'site.standard.publication', 233 + name: leafletPub.name, 234 + url: leafletPub.basePath || 'https://example.com', 235 + description: leafletPub.description, 236 + // ... map other fields 237 + }; 238 + 239 + // 3. Create record via AT Protocol 240 + // (use @atproto/api to create the record) 241 + 242 + console.log('Migration complete!'); 243 + } 244 + ``` 245 + 246 + ### Manual Migration Checklist 247 + 248 + - [ ] Create Standard.site publication record 249 + - [ ] Migrate document records one by one 250 + - [ ] Update slug mappings 251 + - [ ] Test all URLs 252 + - [ ] Verify blog feed 253 + - [ ] Update external links (if any) 254 + - [ ] Archive or delete Leaflet records 255 + 256 + ## Rollback Plan 257 + 258 + If you need to rollback: 259 + 260 + 1. **Revert slug mappings** to use `platform: 'leaflet'` 261 + 2. **Keep Standard.site records** - they won't interfere 262 + 3. **Test Leaflet URLs** to ensure they work again 263 + 264 + ## FAQ 265 + 266 + **Q: Can I migrate gradually?** 267 + A: Yes! Use both platforms side-by-side and migrate publications one at a time. 268 + 269 + **Q: Will my old Leaflet URLs break?** 270 + A: Only if you change the slug mapping. Keep the old mapping to preserve URLs. 271 + 272 + **Q: Can I use the same slug for both platforms?** 273 + A: No, each slug can only map to one publication. Use different slugs (e.g., `blog-old` and `blog-new`). 274 + 275 + **Q: What happens to Bluesky post references?** 276 + A: Standard.site has native support for `bskyPostRef` - you can link documents to Bluesky posts for comments. 277 + 278 + **Q: Do I need to migrate images?** 279 + A: No, blobs in AT Protocol work the same way. Both platforms use the same blob storage. 280 + 281 + **Q: Can I keep using Leaflet's editor?** 282 + A: The Leaflet editor creates Leaflet records. You'll need to use Standard.site-compatible tools or the AT Protocol API directly. 283 + 284 + ## Support Resources 285 + 286 + - [Standard.site Documentation](https://standard.site/) 287 + - [Standard.site Integration Guide](/docs/standard-site-integration.md) 288 + - [Quick Reference](/docs/STANDARD_SITE_QUICK_REF.md) 289 + - [AT Protocol API Docs](https://atproto.com/) 290 + 291 + ## Example Migration Timeline 292 + 293 + **Week 1: Preparation** 294 + - Read documentation 295 + - Test Standard.site locally 296 + - Create test publication 297 + 298 + **Week 2: Pilot Migration** 299 + - Migrate one small publication 300 + - Test thoroughly 301 + - Gather feedback 302 + 303 + **Week 3-4: Full Migration** 304 + - Migrate remaining publications 305 + - Update all slug mappings 306 + - Monitor for issues 307 + 308 + **Week 5: Cleanup** 309 + - Archive Leaflet records 310 + - Update documentation 311 + - Celebrate! 🎉
+29 -3
docs/README.md
··· 27 27 28 28 **Read this if you want to customize or add Colour Themes.** 29 29 30 + ### [Standard.site Integration](./standard-site-integration.md) 31 + 32 + Complete guide to the Standard.site lexicons integration. Covers: 33 + 34 + - Overview of Standard.site lexicon collections 35 + - Type definitions and API reference 36 + - Fetching publications and documents 37 + - Slug-based routing configuration 38 + - Blog feed integration 39 + - URL resolution patterns 40 + - Troubleshooting and examples 41 + 42 + **Read this if you want to use Standard.site for long-form content.** 43 + 44 + **Quick References:** 45 + - [Integration Summary](./INTEGRATION_SUMMARY.md) - What was changed and why 46 + - [Quick Reference](./STANDARD_SITE_QUICK_REF.md) - Common patterns and snippets 47 + - [Migration Guide](./MIGRATION_GUIDE.md) - Migrate from Leaflet to Standard.site 48 + 30 49 ## 🚀 Quick Links 31 50 32 51 - [Main README](../README.md) - Project overview and features ··· 37 56 38 57 ```plaintext 39 58 docs/ 40 - ├── README.md # This file - documentation index 41 - ├── configuration.md # Setup and configuration guide 42 - └── theme-system.md # Theme system documentation 59 + ├── README.md # This file - documentation index 60 + ├── configuration.md # Setup and configuration guide 61 + ├── theme-system.md # Theme system documentation 62 + ├── standard-site-integration.md # Standard.site integration guide 63 + ├── INTEGRATION_SUMMARY.md # Standard.site changes summary 64 + ├── STANDARD_SITE_QUICK_REF.md # Standard.site quick reference 65 + └── MIGRATION_GUIDE.md # Leaflet to Standard.site migration 43 66 ``` 44 67 45 68 ## 💡 Contributing to Documentation ··· 58 81 - [SvelteKit Documentation](https://kit.svelte.dev/) 59 82 - [Tailwind CSS Documentation](https://tailwindcss.com/) 60 83 - [Bluesky](https://bsky.app/) 84 + - [Standard.site](https://standard.site/) - Long-form publishing lexicons 85 + - [Leaflet](https://leaflet.pub/) - Blogging platform for AT Protocol 86 + - [WhiteWind](https://whtwnd.com/) - Alternative blogging platform
+193
docs/STANDARD_SITE_QUICK_REF.md
··· 1 + # Standard.site Quick Reference 2 + 3 + ## Import Functions 4 + 5 + ```typescript 6 + import { 7 + fetchStandardSitePublications, 8 + fetchStandardSiteDocuments, 9 + type StandardSitePublication, 10 + type StandardSiteDocument 11 + } from '$lib/services/atproto'; 12 + ``` 13 + 14 + ## Fetch Publications 15 + 16 + ```typescript 17 + const { publications } = await fetchStandardSitePublications(); 18 + 19 + publications.forEach(pub => { 20 + console.log(pub.name); // Publication name 21 + console.log(pub.url); // Base URL 22 + console.log(pub.icon); // Icon blob URL 23 + console.log(pub.basicTheme); // Theme colors 24 + }); 25 + ``` 26 + 27 + ## Fetch Documents 28 + 29 + ```typescript 30 + const { documents } = await fetchStandardSiteDocuments(); 31 + 32 + documents.forEach(doc => { 33 + console.log(doc.title); // Document title 34 + console.log(doc.url); // Full canonical URL 35 + console.log(doc.publishedAt); // ISO timestamp 36 + console.log(doc.coverImage); // Cover image blob URL 37 + console.log(doc.tags); // Array of tags 38 + console.log(doc.publicationName); // Parent publication name 39 + }); 40 + ``` 41 + 42 + ## Configure Slug Mapping 43 + 44 + **File**: `/src/lib/data/slug-mappings.ts` 45 + 46 + ```typescript 47 + export const slugMappings: SlugMapping[] = [ 48 + { 49 + slug: 'my-blog', 50 + publicationRkey: '3labc123xyz', 51 + platform: 'standard.site' // ← Required for Standard.site 52 + } 53 + ]; 54 + ``` 55 + 56 + ## Get Publication from Slug 57 + 58 + ```typescript 59 + import { getPublicationFromSlug } from '$lib/config/slugs'; 60 + 61 + const info = getPublicationFromSlug('my-blog'); 62 + // Returns: { rkey: '3labc123xyz', platform: 'standard.site' } 63 + ``` 64 + 65 + ## Access via URLs 66 + 67 + Once configured, content is accessible at: 68 + 69 + - **Publication**: `https://yoursite.com/my-blog` 70 + - Redirects to publication URL 71 + 72 + - **Document**: `https://yoursite.com/my-blog/3labc123xyz` 73 + - Redirects to `{publication.url}{document.path}` 74 + 75 + ## Theme Colors 76 + 77 + ```typescript 78 + const theme = publication.basicTheme; 79 + 80 + // Convert to CSS 81 + const bgColor = `rgb(${theme.background.r}, ${theme.background.g}, ${theme.background.b})`; 82 + const fgColor = `rgb(${theme.foreground.r}, ${theme.foreground.g}, ${theme.foreground.b})`; 83 + const acColor = `rgb(${theme.accent.r}, ${theme.accent.g}, ${theme.accent.b})`; 84 + ``` 85 + 86 + ## Blog Feed Integration 87 + 88 + ```typescript 89 + import { fetchBlogPosts } from '$lib/services/atproto'; 90 + 91 + const { posts } = await fetchBlogPosts(); 92 + 93 + // Filter by platform 94 + const standardPosts = posts.filter(p => p.platform === 'standard.site'); 95 + const leafletPosts = posts.filter(p => p.platform === 'leaflet'); 96 + const whiteWindPosts = posts.filter(p => p.platform === 'WhiteWind'); 97 + ``` 98 + 99 + ## Document URL Patterns 100 + 101 + ### Publication-Based Document 102 + ```typescript 103 + { 104 + site: 'at://did:plc:abc/site.standard.publication/xyz', 105 + path: '/my-post' 106 + } 107 + // URL: {publication.url}/my-post 108 + ``` 109 + 110 + ### Loose Document 111 + ```typescript 112 + { 113 + site: 'https://myblog.com', 114 + path: '/standalone-post' 115 + } 116 + // URL: https://myblog.com/standalone-post 117 + ``` 118 + 119 + ## Collections 120 + 121 + | Collection | Description | 122 + |------------|-------------| 123 + | `site.standard.publication` | Publication/blog metadata | 124 + | `site.standard.document` | Individual posts/articles | 125 + 126 + ## Cache Keys 127 + 128 + | Data | Cache Key | 129 + |------|-----------| 130 + | Publications | `standard-site:publications:{DID}` | 131 + | Documents | `standard-site:documents:{DID}` | 132 + 133 + ## Common Patterns 134 + 135 + ### Display Publication Icon 136 + ```svelte 137 + {#if publication.icon} 138 + <img src={publication.icon} alt={publication.name} /> 139 + {/if} 140 + ``` 141 + 142 + ### Display Document Cover 143 + ```svelte 144 + {#if document.coverImage} 145 + <img src={document.coverImage} alt={document.title} /> 146 + {/if} 147 + ``` 148 + 149 + ### Display Tags 150 + ```svelte 151 + {#if document.tags} 152 + <div class="tags"> 153 + {#each document.tags as tag} 154 + <span class="tag">{tag}</span> 155 + {/each} 156 + </div> 157 + {/if} 158 + ``` 159 + 160 + ### Format Date 161 + ```svelte 162 + <time datetime={document.publishedAt}> 163 + {new Date(document.publishedAt).toLocaleDateString()} 164 + </time> 165 + ``` 166 + 167 + ## Platform Detection 168 + 169 + ```typescript 170 + // In route handlers 171 + import type { PublicationPlatform } from '$lib/data/slug-mappings'; 172 + 173 + if (platform === 'standard.site') { 174 + // Handle Standard.site 175 + } else if (platform === 'leaflet') { 176 + // Handle Leaflet 177 + } 178 + ``` 179 + 180 + ## Troubleshooting 181 + 182 + | Issue | Solution | 183 + |-------|----------| 184 + | Documents not showing | Check `publishedAt` is set | 185 + | Redirects not working | Verify publication `url` starts with `http://` or `https://` | 186 + | Images not loading | Check blob CIDs and PDS resolution | 187 + | Slug not found | Add mapping to `slug-mappings.ts` with `platform: 'standard.site'` | 188 + 189 + ## Resources 190 + 191 + - [Standard.site Docs](https://standard.site/) 192 + - [Full Integration Guide](/docs/standard-site-integration.md) 193 + - [AT Protocol Lexicons](https://atproto.com/specs/lexicon)
+290
docs/standard-site-integration.md
··· 1 + # Standard.site Lexicons Integration 2 + 3 + This document describes the integration of [Standard.site](https://standard.site/) lexicons into the website, enabling support for AT Protocol-based long-form content alongside Leaflet and WhiteWind. 4 + 5 + ## Overview 6 + 7 + Standard.site provides shared lexicon schemas for long-form publishing on the AT Protocol. The integration allows your website to: 8 + 9 + - Fetch and display Standard.site publications 10 + - Fetch and display Standard.site documents 11 + - Use slug-based routing for Standard.site content 12 + - Mix Standard.site content with Leaflet and WhiteWind in blog feeds 13 + 14 + ## Lexicon Collections 15 + 16 + The integration supports the following Standard.site lexicon collections: 17 + 18 + ### `site.standard.publication` 19 + Publications represent websites, blogs, or content platforms. Each publication defines: 20 + - `name`: Publication name 21 + - `url`: Base URL for the publication 22 + - `description`: Brief description 23 + - `icon`: Square publication icon (at least 256x256) 24 + - `basicTheme`: Simplified theme with background, foreground, accent colors 25 + - `preferences`: Platform preferences (e.g., `showInDiscover`) 26 + 27 + ### `site.standard.document` 28 + Documents represent individual articles, posts, or content. Each document includes: 29 + - `title`: Document title 30 + - `site`: Publication reference (AT URI) or standalone URL 31 + - `path`: Document path (combined with site for canonical URL) 32 + - `description`: Brief description or excerpt 33 + - `coverImage`: Thumbnail or cover image 34 + - `content`: Open union for various content formats 35 + - `textContent`: Plaintext representation 36 + - `bskyPostRef`: Reference to Bluesky post for comments 37 + - `tags`: Array of tags (without hashtags) 38 + - `publishedAt`: Publish timestamp (required) 39 + - `updatedAt`: Last edit timestamp 40 + 41 + ## File Structure 42 + 43 + ### New Files 44 + - `/src/lib/services/atproto/standard.ts` - Standard.site-specific fetch functions 45 + - `/docs/standard-site-integration.md` - This documentation 46 + 47 + ### Modified Files 48 + - `/src/lib/services/atproto/types.ts` - Added Standard.site types 49 + - `/src/lib/services/atproto/index.ts` - Exported Standard.site functions 50 + - `/src/lib/services/atproto/posts.ts` - Integrated Standard.site into blog feed 51 + - `/src/lib/data/slug-mappings.ts` - Added platform support 52 + - `/src/lib/config/slugs.ts` - Added platform-aware functions 53 + - `/src/routes/[slug=slug]/+server.ts` - Added Standard.site redirect support 54 + - `/src/routes/[slug=slug]/[rkey]/+server.ts` - Added Standard.site document routing 55 + 56 + ## Usage 57 + 58 + ### 1. Configure Slug Mappings 59 + 60 + Add Standard.site publications to your slug mappings in `/src/lib/data/slug-mappings.ts`: 61 + 62 + ```typescript 63 + export const slugMappings: SlugMapping[] = [ 64 + { 65 + slug: 'blog', 66 + publicationRkey: 'abc123xyz', 67 + platform: 'standard.site' 68 + }, 69 + { 70 + slug: 'notes', 71 + publicationRkey: 'def456uvw', 72 + platform: 'leaflet' 73 + } 74 + ]; 75 + ``` 76 + 77 + ### 2. Fetch Standard.site Data 78 + 79 + Use the provided fetch functions in your components or routes: 80 + 81 + ```typescript 82 + import { 83 + fetchStandardSitePublications, 84 + fetchStandardSiteDocuments 85 + } from '$lib/services/atproto'; 86 + 87 + // Fetch all publications 88 + const { publications } = await fetchStandardSitePublications(); 89 + 90 + // Fetch all documents 91 + const { documents } = await fetchStandardSiteDocuments(); 92 + 93 + // Documents are automatically sorted by publishedAt (newest first) 94 + ``` 95 + 96 + ### 3. Access via Slug Routes 97 + 98 + Standard.site content is accessible via slug routes: 99 + 100 + - `/{slug}` - Redirects to the publication URL 101 + - `/{slug}/{rkey}` - Redirects to the specific document URL 102 + 103 + Example: 104 + - `/blog` → Redirects to the Standard.site publication URL 105 + - `/blog/3labc123xyz` → Redirects to the document at publication URL + document path 106 + 107 + ## Type Definitions 108 + 109 + ### StandardSitePublication 110 + ```typescript 111 + interface StandardSitePublication { 112 + name: string; 113 + rkey: string; 114 + uri: string; 115 + url: string; 116 + description?: string; 117 + icon?: string; 118 + basicTheme?: StandardSiteBasicTheme; 119 + preferences?: { 120 + showInDiscover?: boolean; 121 + }; 122 + } 123 + ``` 124 + 125 + ### StandardSiteDocument 126 + ```typescript 127 + interface StandardSiteDocument { 128 + title: string; 129 + rkey: string; 130 + uri: string; 131 + url: string; 132 + site: string; 133 + path?: string; 134 + description?: string; 135 + coverImage?: string; 136 + content?: any; 137 + textContent?: string; 138 + bskyPostRef?: { 139 + uri: string; 140 + cid: string; 141 + }; 142 + tags?: string[]; 143 + publishedAt: string; 144 + updatedAt?: string; 145 + publicationName?: string; 146 + publicationRkey?: string; 147 + } 148 + ``` 149 + 150 + ### StandardSiteBasicTheme 151 + ```typescript 152 + interface StandardSiteBasicTheme { 153 + background: StandardSiteThemeColor; 154 + foreground: StandardSiteThemeColor; 155 + accent: StandardSiteThemeColor; 156 + accentForeground: StandardSiteThemeColor; 157 + } 158 + 159 + interface StandardSiteThemeColor { 160 + r: number; // 0-255 161 + g: number; // 0-255 162 + b: number; // 0-255 163 + a?: number; // 0-100 (only for rgba) 164 + } 165 + ``` 166 + 167 + ## Integration with Blog Feed 168 + 169 + Standard.site documents are automatically included in the unified blog feed alongside Leaflet and WhiteWind posts: 170 + 171 + ```typescript 172 + const { posts } = await fetchBlogPosts(); 173 + 174 + // Posts include platform property: 'WhiteWind' | 'leaflet' | 'standard.site' 175 + posts.forEach(post => { 176 + console.log(`${post.title} from ${post.platform}`); 177 + }); 178 + ``` 179 + 180 + ## URL Resolution 181 + 182 + Standard.site documents support two URL patterns: 183 + 184 + ### 1. Publication-Based 185 + When `site` points to a publication record (`at://...`): 186 + ``` 187 + {publication.url}{document.path} 188 + ``` 189 + 190 + Example: 191 + - Publication URL: `https://myblog.com` 192 + - Document path: `/my-first-post` 193 + - Result: `https://myblog.com/my-first-post` 194 + 195 + ### 2. Loose Documents 196 + When `site` is a direct URL: 197 + ``` 198 + {site}{document.path} 199 + ``` 200 + 201 + Example: 202 + - Site: `https://myblog.com` 203 + - Document path: `/standalone-post` 204 + - Result: `https://myblog.com/standalone-post` 205 + 206 + ## Caching 207 + 208 + All Standard.site data is cached using the same cache system as other AT Protocol services: 209 + 210 + - Publications: Cached with key `standard-site:publications:{DID}` 211 + - Documents: Cached with key `standard-site:documents:{DID}` 212 + 213 + Cache entries follow the standard TTL and can be invalidated via the cache service. 214 + 215 + ## Example: Creating a Standard.site Document Component 216 + 217 + ```svelte 218 + <script lang="ts"> 219 + import type { StandardSiteDocument } from '$lib/services/atproto'; 220 + 221 + export let document: StandardSiteDocument; 222 + </script> 223 + 224 + <article> 225 + <h1>{document.title}</h1> 226 + 227 + {#if document.description} 228 + <p class="description">{document.description}</p> 229 + {/if} 230 + 231 + {#if document.coverImage} 232 + <img src={document.coverImage} alt={document.title} /> 233 + {/if} 234 + 235 + <div class="meta"> 236 + <time datetime={document.publishedAt}> 237 + {new Date(document.publishedAt).toLocaleDateString()} 238 + </time> 239 + 240 + {#if document.tags && document.tags.length > 0} 241 + <div class="tags"> 242 + {#each document.tags as tag} 243 + <span class="tag">{tag}</span> 244 + {/each} 245 + </div> 246 + {/if} 247 + </div> 248 + 249 + {#if document.textContent} 250 + <div class="content"> 251 + {document.textContent} 252 + </div> 253 + {/if} 254 + 255 + <a href={document.url}>Read full article →</a> 256 + </article> 257 + ``` 258 + 259 + ## Resources 260 + 261 + - [Standard.site](https://standard.site/) - Full specification and documentation 262 + - [Standard.site Lexicons Repository](https://github.com/standard-site/lexicons) 263 + - [AT Protocol](https://atproto.com/) - The underlying protocol 264 + - [Lexicon Documentation](https://atproto.com/specs/lexicon) - AT Protocol lexicon spec 265 + 266 + ## Migration Notes 267 + 268 + If you're migrating content from Leaflet to Standard.site: 269 + 270 + 1. Both platforms can coexist - no need to choose one 271 + 2. Update slug mappings to specify `platform: 'standard.site'` for new publications 272 + 3. Standard.site uses `publishedAt` instead of Leaflet's timestamp field 273 + 4. Standard.site documents have a more flexible content model via the open union 274 + 275 + ## Troubleshooting 276 + 277 + ### Documents not appearing in feed 278 + - Verify the document has a valid `publishedAt` timestamp 279 + - Check that the document's `site` field correctly references your publication 280 + - Ensure the publication is correctly configured in slug mappings 281 + 282 + ### Redirect not working 283 + - Confirm the publication URL is properly formatted (should start with `http://` or `https://`) 284 + - Check that the document's `path` field is set (defaults to `/{rkey}` if omitted) 285 + - Verify the slug mapping has the correct `publicationRkey` and `platform: 'standard.site'` 286 + 287 + ### Images not loading 288 + - Ensure blob URLs are correctly resolved through the PDS 289 + - Check that the `icon` and `coverImage` blobs are properly uploaded 290 + - Verify blob CIDs are correctly referenced in the lexicon records
+2 -2
package-lock.json
··· 1 1 { 2 2 "name": "website", 3 - "version": "10.5.0", 3 + "version": "10.6.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "website", 9 - "version": "10.5.0", 9 + "version": "10.6.0", 10 10 "dependencies": { 11 11 "@atproto/api": "^0.18.1", 12 12 "@lucide/svelte": "^0.554.0",
+1 -1
package.json
··· 1 1 { 2 2 "name": "website", 3 3 "private": true, 4 - "version": "10.5.0", 4 + "version": "10.6.0", 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite dev",
+2 -2
src/lib/components/layout/Footer.svelte
··· 97 97 type="button" 98 98 onclick={() => happyMacStore.incrementClick()} 99 99 class="cursor-default select-none transition-colors hover:text-ink-600 dark:hover:text-ink-300" 100 - aria-label="Version 10.5.0{showHint ? ` - ${$happyMacStore.clickCount} of 24 clicks` : ''}" 100 + aria-label="Version 10.6.0{showHint ? ` - ${$happyMacStore.clickCount} of 24 clicks` : ''}" 101 101 title={showHint ? `${$happyMacStore.clickCount}/24` : ''} 102 102 > 103 - v10.5.0{#if showHint}<span class="ml-1 text-xs opacity-60">({$happyMacStore.clickCount}/24)</span>{/if} 103 + v10.6.0{#if showHint}<span class="ml-1 text-xs opacity-60">({$happyMacStore.clickCount}/24)</span>{/if} 104 104 </button> 105 105 </div> 106 106 </div>
+21 -5
src/lib/config/slugs.ts
··· 1 - import { slugMappings, type SlugMapping } from '$lib/data/slug-mappings'; 1 + import { slugMappings, type SlugMapping, type PublicationPlatform } from '$lib/data/slug-mappings'; 2 2 3 3 /** 4 4 * Normalize a slug to be URI-compatible ··· 29 29 } 30 30 31 31 /** 32 - * Get publication rkey from slug 32 + * Get publication info from slug 33 33 * Automatically normalizes the slug before lookup 34 34 * 35 35 * @param slug - The slug to look up (will be normalized) 36 - * @returns The publication rkey or null if not found 36 + * @returns Object with rkey and platform, or null if not found 37 37 */ 38 - export function getPublicationRkeyFromSlug(slug: string): string | null { 38 + export function getPublicationFromSlug(slug: string): { rkey: string; platform: PublicationPlatform } | null { 39 39 const normalizedSlug = normalizeSlug(slug); 40 40 const mapping = slugMappings.find((m) => normalizeSlug(m.slug) === normalizedSlug); 41 - return mapping?.publicationRkey || null; 41 + if (!mapping) return null; 42 + return { 43 + rkey: mapping.publicationRkey, 44 + platform: mapping.platform || 'leaflet' // Default to leaflet for backwards compatibility 45 + }; 46 + } 47 + 48 + /** 49 + * Get publication rkey from slug (backwards compatibility) 50 + * Automatically normalizes the slug before lookup 51 + * 52 + * @param slug - The slug to look up (will be normalized) 53 + * @returns The publication rkey or null if not found 54 + */ 55 + export function getPublicationRkeyFromSlug(slug: string): string | null { 56 + const result = getPublicationFromSlug(slug); 57 + return result?.rkey || null; 42 58 } 43 59 44 60 /**
+18 -11
src/lib/data/slug-mappings.ts
··· 1 1 /** 2 - * Slug to Leaflet Publication mapping data 2 + * Slug to Publication mapping data 3 3 * 4 - * Maps friendly URL slugs to Leaflet publication rkeys. 5 - * This allows you to access publications via /{slug} instead of /blog 4 + * Maps friendly URL slugs to publication rkeys from Leaflet or Standard.site. 5 + * This allows you to access publications via /{slug} instead of using rkeys. 6 6 * 7 7 * Example: 8 - * - /blog → maps to publication with rkey "3m3x4bgbsh22k" 9 - * - /notes → maps to publication with rkey "xyz123abc" 8 + * - /blog → maps to Leaflet publication with rkey "3m3x4bgbsh22k" 9 + * - /notes → maps to Standard.site publication with rkey "xyz123abc" 10 10 */ 11 + 12 + export type PublicationPlatform = 'leaflet' | 'standard.site'; 11 13 12 14 export interface SlugMapping { 13 15 /** The URL-friendly slug (will be normalized automatically) */ 14 16 slug: string; 15 - /** The Leaflet publication rkey */ 17 + /** The publication rkey */ 16 18 publicationRkey: string; 19 + /** The platform this publication belongs to (defaults to 'leaflet' for backwards compatibility) */ 20 + platform?: PublicationPlatform; 17 21 } 18 22 19 23 /** ··· 29 33 export const slugMappings: SlugMapping[] = [ 30 34 { 31 35 slug: 'blog', 32 - publicationRkey: '3m3x4bgbsh22k' // my blog publication rkey 36 + publicationRkey: '3m3x4bgbsh22k', // my blog publication rkey 37 + platform: 'leaflet' 33 38 }, 34 39 { 35 40 slug: 'cailean', 36 - publicationRkey: '3m4222fxc3k2q' // Cailean Uen's publication rkey for his journal 41 + publicationRkey: '3m4222fxc3k2q', // Cailean Uen's publication rkey for his journal 42 + platform: 'leaflet' 37 43 }, 38 44 { 39 45 slug: 'creativity', 40 - publicationRkey: '3m6afhzlxt22p' // my creativity dump publication rkey 46 + publicationRkey: '3m6afhzlxt22p', // my creativity dump publication rkey 47 + platform: 'leaflet' 41 48 } 42 49 // Add more mappings as needed: 43 - // { slug: 'notes', publicationRkey: 'xyz123abc' }, 44 - // { slug: 'essays', publicationRkey: 'def456ghi' }, 50 + // { slug: 'notes', publicationRkey: 'xyz123abc', platform: 'standard.site' }, 51 + // { slug: 'essays', publicationRkey: 'def456ghi', platform: 'leaflet' }, 45 52 ];
+12 -1
src/lib/services/atproto/index.ts
··· 32 32 MusicArtist, 33 33 KibunStatusData, 34 34 TangledRepo, 35 - TangledReposData 35 + TangledReposData, 36 + StandardSitePublication, 37 + StandardSitePublicationsData, 38 + StandardSiteDocument, 39 + StandardSiteDocumentsData, 40 + StandardSiteBasicTheme, 41 + StandardSiteThemeColor 36 42 } from './types'; 37 43 38 44 // Export fetch functions ··· 44 50 fetchKibunStatus, 45 51 fetchTangledRepos 46 52 } from './fetch'; 53 + 54 + export { 55 + fetchStandardSitePublications, 56 + fetchStandardSiteDocuments 57 + } from './standard'; 47 58 48 59 export { 49 60 fetchBlogPosts,
+23 -2
src/lib/services/atproto/posts.ts
··· 13 13 LeafletPublication, 14 14 LeafletPublicationsData 15 15 } from './types'; 16 + import { fetchStandardSiteDocuments } from './standard'; 16 17 17 18 /** 18 19 * Fetches all Leaflet publications for a user ··· 87 88 } 88 89 89 90 /** 90 - * Fetches blog posts from both WhiteWind and Leaflet sources 91 - * Now supports multiple Leaflet publications 91 + * Fetches blog posts from WhiteWind, Leaflet, and Standard.site sources 92 + * Supports multiple publications from all platforms 92 93 */ 93 94 export async function fetchBlogPosts(fetchFn?: typeof fetch): Promise<BlogPostsData> { 94 95 const cacheKey = `blogposts:${PUBLIC_ATPROTO_DID}`; ··· 191 192 } 192 193 } catch (error) { 193 194 console.warn('Failed to fetch Leaflet documents:', error); 195 + } 196 + 197 + // Fetch Standard.site documents 198 + try { 199 + const standardDocumentsData = await fetchStandardSiteDocuments(fetchFn); 200 + 201 + for (const doc of standardDocumentsData.documents) { 202 + posts.push({ 203 + title: doc.title, 204 + url: doc.url, 205 + createdAt: doc.publishedAt, 206 + platform: 'standard.site', 207 + description: doc.description, 208 + rkey: doc.rkey, 209 + publicationName: doc.publicationName, 210 + publicationRkey: doc.publicationRkey 211 + }); 212 + } 213 + } catch (error) { 214 + console.warn('Failed to fetch Standard.site documents:', error); 194 215 } 195 216 196 217 // Sort by date (newest first) and take top 5
+208
src/lib/services/atproto/standard.ts
··· 1 + import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 2 + import { cache } from './cache'; 3 + import { withFallback, resolveIdentity } from './agents'; 4 + import { buildPdsBlobUrl } from './media'; 5 + import type { 6 + StandardSitePublication, 7 + StandardSitePublicationsData, 8 + StandardSiteDocument, 9 + StandardSiteDocumentsData, 10 + StandardSiteBasicTheme, 11 + StandardSiteThemeColor 12 + } from './types'; 13 + 14 + /** 15 + * Fetches all Standard.site publications for a user 16 + */ 17 + export async function fetchStandardSitePublications( 18 + fetchFn?: typeof fetch 19 + ): Promise<StandardSitePublicationsData> { 20 + console.info('[Standard.site] Fetching publications'); 21 + const cacheKey = `standard-site:publications:${PUBLIC_ATPROTO_DID}`; 22 + const cached = cache.get<StandardSitePublicationsData>(cacheKey); 23 + if (cached) { 24 + console.debug('[Standard.site] Returning cached publications'); 25 + return cached; 26 + } 27 + 28 + const publications: StandardSitePublication[] = []; 29 + console.info('[Standard.site] Cache miss, fetching from network'); 30 + 31 + try { 32 + console.debug('[Standard.site] Querying publications records'); 33 + const publicationsRecords = await withFallback( 34 + PUBLIC_ATPROTO_DID, 35 + async (agent) => { 36 + const response = await agent.com.atproto.repo.listRecords({ 37 + repo: PUBLIC_ATPROTO_DID, 38 + collection: 'site.standard.publication', 39 + limit: 100 40 + }); 41 + return response.data.records; 42 + }, 43 + true, 44 + fetchFn 45 + ); 46 + 47 + for (const pubRecord of publicationsRecords) { 48 + const pubValue = pubRecord.value as any; 49 + const rkey = pubRecord.uri.split('/').pop() || ''; 50 + 51 + // Extract basic theme if present 52 + let basicTheme: StandardSiteBasicTheme | undefined; 53 + if (pubValue.basicTheme) { 54 + const theme = pubValue.basicTheme; 55 + basicTheme = { 56 + background: theme.background as StandardSiteThemeColor, 57 + foreground: theme.foreground as StandardSiteThemeColor, 58 + accent: theme.accent as StandardSiteThemeColor, 59 + accentForeground: theme.accentForeground as StandardSiteThemeColor 60 + }; 61 + } 62 + 63 + publications.push({ 64 + name: pubValue.name || 'Untitled Publication', 65 + rkey, 66 + uri: pubRecord.uri, 67 + url: pubValue.url, 68 + description: pubValue.description, 69 + icon: pubValue.icon ? await getBlobUrl(pubValue.icon, fetchFn) : undefined, 70 + basicTheme, 71 + preferences: pubValue.preferences 72 + }); 73 + } 74 + 75 + const data: StandardSitePublicationsData = { publications }; 76 + cache.set(cacheKey, data); 77 + return data; 78 + } catch (error) { 79 + console.warn('Failed to fetch Standard.site publications:', error); 80 + return { publications: [] }; 81 + } 82 + } 83 + 84 + /** 85 + * Fetches all Standard.site documents for a user 86 + */ 87 + export async function fetchStandardSiteDocuments( 88 + fetchFn?: typeof fetch 89 + ): Promise<StandardSiteDocumentsData> { 90 + console.info('[Standard.site] Fetching documents'); 91 + const cacheKey = `standard-site:documents:${PUBLIC_ATPROTO_DID}`; 92 + const cached = cache.get<StandardSiteDocumentsData>(cacheKey); 93 + if (cached) { 94 + console.debug('[Standard.site] Returning cached documents'); 95 + return cached; 96 + } 97 + 98 + const documents: StandardSiteDocument[] = []; 99 + console.info('[Standard.site] Cache miss, fetching from network'); 100 + 101 + try { 102 + // Get all publications first to map URIs to publication data 103 + const publicationsData = await fetchStandardSitePublications(fetchFn); 104 + const publicationsMap = new Map<string, StandardSitePublication>(); 105 + for (const pub of publicationsData.publications) { 106 + publicationsMap.set(pub.uri, pub); 107 + } 108 + 109 + console.debug('[Standard.site] Querying documents records'); 110 + const documentsRecords = await withFallback( 111 + PUBLIC_ATPROTO_DID, 112 + async (agent) => { 113 + const response = await agent.com.atproto.repo.listRecords({ 114 + repo: PUBLIC_ATPROTO_DID, 115 + collection: 'site.standard.document', 116 + limit: 100 117 + }); 118 + return response.data.records; 119 + }, 120 + true, 121 + fetchFn 122 + ); 123 + 124 + for (const docRecord of documentsRecords) { 125 + const docValue = docRecord.value as any; 126 + const rkey = docRecord.uri.split('/').pop() || ''; 127 + 128 + // Determine the publication info 129 + const siteValue = docValue.site; 130 + let publication: StandardSitePublication | undefined; 131 + let publicationRkey: string | undefined; 132 + let url: string; 133 + 134 + // Check if site points to a publication record (at://) or a URL (https://) 135 + if (siteValue.startsWith('at://')) { 136 + // It's a publication URI 137 + publication = publicationsMap.get(siteValue); 138 + publicationRkey = siteValue.split('/').pop(); 139 + 140 + // Build URL from publication base URL + document path 141 + if (publication) { 142 + const basePath = publication.url.endsWith('/') 143 + ? publication.url.slice(0, -1) 144 + : publication.url; 145 + const docPath = docValue.path || `/${rkey}`; 146 + url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 147 + } else { 148 + // Fallback if publication not found 149 + url = `${siteValue}${docValue.path || '/' + rkey}`; 150 + } 151 + } else { 152 + // It's a loose document with a direct URL 153 + const basePath = siteValue.endsWith('/') ? siteValue.slice(0, -1) : siteValue; 154 + const docPath = docValue.path || `/${rkey}`; 155 + url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 156 + } 157 + 158 + documents.push({ 159 + title: docValue.title || 'Untitled Document', 160 + rkey, 161 + uri: docRecord.uri, 162 + url, 163 + site: docValue.site, 164 + path: docValue.path, 165 + description: docValue.description, 166 + coverImage: docValue.coverImage 167 + ? await getBlobUrl(docValue.coverImage, fetchFn) 168 + : undefined, 169 + content: docValue.content, 170 + textContent: docValue.textContent, 171 + bskyPostRef: docValue.bskyPostRef, 172 + tags: docValue.tags, 173 + publishedAt: docValue.publishedAt, 174 + updatedAt: docValue.updatedAt, 175 + publicationName: publication?.name, 176 + publicationRkey 177 + }); 178 + } 179 + 180 + // Sort by publishedAt (newest first) 181 + documents.sort( 182 + (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime() 183 + ); 184 + 185 + const data: StandardSiteDocumentsData = { documents }; 186 + cache.set(cacheKey, data); 187 + return data; 188 + } catch (error) { 189 + console.warn('Failed to fetch Standard.site documents:', error); 190 + return { documents: [] }; 191 + } 192 + } 193 + 194 + /** 195 + * Helper function to get a blob URL for Standard.site publication icons and document cover images 196 + */ 197 + async function getBlobUrl(blob: any, fetchFn?: typeof fetch): Promise<string | undefined> { 198 + try { 199 + const cid = blob.ref?.$link || blob.cid; 200 + if (!cid) return undefined; 201 + 202 + const resolved = await resolveIdentity(PUBLIC_ATPROTO_DID, fetchFn); 203 + return buildPdsBlobUrl(resolved.pds, PUBLIC_ATPROTO_DID, cid); 204 + } catch (error) { 205 + console.warn('Failed to resolve blob URL:', error); 206 + return undefined; 207 + } 208 + }
+59 -1
src/lib/services/atproto/types.ts
··· 102 102 title: string; 103 103 url: string; 104 104 createdAt: string; 105 - platform: 'WhiteWind' | 'leaflet'; 105 + platform: 'WhiteWind' | 'leaflet' | 'standard.site'; 106 106 description?: string; 107 107 rkey: string; 108 108 publicationName?: string; ··· 240 240 export interface TangledReposData { 241 241 repos: TangledRepo[]; 242 242 } 243 + 244 + // Standard.site types 245 + export interface StandardSiteThemeColor { 246 + r: number; 247 + g: number; 248 + b: number; 249 + a?: number; 250 + } 251 + 252 + export interface StandardSiteBasicTheme { 253 + background: StandardSiteThemeColor; 254 + foreground: StandardSiteThemeColor; 255 + accent: StandardSiteThemeColor; 256 + accentForeground: StandardSiteThemeColor; 257 + } 258 + 259 + export interface StandardSitePublication { 260 + name: string; 261 + rkey: string; 262 + uri: string; 263 + url: string; 264 + description?: string; 265 + icon?: string; 266 + basicTheme?: StandardSiteBasicTheme; 267 + preferences?: { 268 + showInDiscover?: boolean; 269 + }; 270 + } 271 + 272 + export interface StandardSitePublicationsData { 273 + publications: StandardSitePublication[]; 274 + } 275 + 276 + export interface StandardSiteDocument { 277 + title: string; 278 + rkey: string; 279 + uri: string; 280 + url: string; 281 + site: string; 282 + path?: string; 283 + description?: string; 284 + coverImage?: string; 285 + content?: any; 286 + textContent?: string; 287 + bskyPostRef?: { 288 + uri: string; 289 + cid: string; 290 + }; 291 + tags?: string[]; 292 + publishedAt: string; 293 + updatedAt?: string; 294 + publicationName?: string; 295 + publicationRkey?: string; 296 + } 297 + 298 + export interface StandardSiteDocumentsData { 299 + documents: StandardSiteDocument[]; 300 + }
+40 -24
src/routes/[slug=slug]/+server.ts
··· 1 1 import type { RequestHandler } from '@sveltejs/kit'; 2 2 import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 3 - import { fetchLeafletPublications } from '$lib/services/atproto'; 4 - import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; 3 + import { fetchLeafletPublications, fetchStandardSitePublications } from '$lib/services/atproto'; 4 + import { getPublicationFromSlug } from '$lib/config/slugs'; 5 5 6 6 /** 7 7 * Dynamic slug root redirect handler 8 8 * 9 - * Redirects /{slug} to the appropriate Leaflet publication: 10 - * - Uses the slug mapping config to find the publication rkey 11 - * - Priority 1: Publication base_path from Leaflet API 12 - * - Priority 2: Leaflet /lish format 9 + * Redirects /{slug} to the appropriate publication (Leaflet or Standard.site): 10 + * - Uses the slug mapping config to find the publication rkey and platform 11 + * - For Leaflet: Priority 1: Publication base_path, Priority 2: /lish format 12 + * - For Standard.site: Uses the publication URL directly 13 13 * 14 14 * Individual posts are handled by the [rkey] route. 15 15 */ ··· 31 31 32 32 // For /{slug} root, redirect to the publication 33 33 if (!slugPath || slugPath === '') { 34 - // Validate slug and get the publication rkey 34 + // Validate slug and get the publication info 35 35 if (!slug) { 36 36 return new Response('Invalid slug', { 37 37 status: 400, ··· 41 41 }); 42 42 } 43 43 44 - const publicationRkey = getPublicationRkeyFromSlug(slug); 44 + const publicationInfo = getPublicationFromSlug(slug); 45 45 46 - if (!publicationRkey) { 46 + if (!publicationInfo) { 47 47 return new Response( 48 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, 48 + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 49 49 { 50 50 status: 404, 51 51 headers: { ··· 55 55 ); 56 56 } 57 57 58 + const { rkey: publicationRkey, platform } = publicationInfo; 58 59 let redirectUrl: string | null = null; 59 60 60 61 try { 61 - // Fetch publications to get base path 62 - const { publications } = await fetchLeafletPublications(); 63 - const publication = publications.find((p) => p.rkey === publicationRkey); 62 + if (platform === 'standard.site') { 63 + // Fetch Standard.site publications 64 + const { publications } = await fetchStandardSitePublications(); 65 + const publication = publications.find((p) => p.rkey === publicationRkey); 64 66 65 - if (publication?.basePath) { 66 - // Ensure basePath is a complete URL 67 - redirectUrl = publication.basePath.startsWith('http') 68 - ? publication.basePath 69 - : `https://${publication.basePath}`; 67 + if (publication) { 68 + // Use the publication URL directly 69 + redirectUrl = publication.url.startsWith('http') 70 + ? publication.url 71 + : `https://${publication.url}`; 72 + } 70 73 } else { 71 - // Use Leaflet /lish format 72 - redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; 74 + // Fetch Leaflet publications 75 + const { publications } = await fetchLeafletPublications(); 76 + const publication = publications.find((p) => p.rkey === publicationRkey); 77 + 78 + if (publication?.basePath) { 79 + // Ensure basePath is a complete URL 80 + redirectUrl = publication.basePath.startsWith('http') 81 + ? publication.basePath 82 + : `https://${publication.basePath}`; 83 + } else { 84 + // Use Leaflet /lish format 85 + redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; 86 + } 73 87 } 74 88 } catch (error) { 75 - console.error('Error fetching Leaflet publication:', error); 76 - // Fallback to /lish format 77 - redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; 89 + console.error(`Error fetching ${platform} publication:`, error); 90 + // Fallback based on platform 91 + if (platform === 'leaflet') { 92 + redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; 93 + } 78 94 } 79 95 80 96 // If we have a redirect URL, use it ··· 90 106 91 107 // No publication found 92 108 return new Response( 93 - `Publication not found for slug: ${slug}\n\nPlease check your configuration in src/lib/config/slugs.ts`, 109 + `Publication not found for slug: ${slug}\n\nPlease check your configuration in src/lib/data/slug-mappings.ts`, 94 110 { 95 111 status: 404, 96 112 headers: {
+86 -17
src/routes/[slug=slug]/[rkey]/+server.ts
··· 5 5 PUBLIC_ENABLE_WHITEWIND 6 6 } from '$env/static/public'; 7 7 import { withFallback } from '$lib/services/atproto'; 8 - import { fetchLeafletPublications } from '$lib/services/atproto'; 9 - import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; 8 + import { fetchLeafletPublications, fetchStandardSitePublications } from '$lib/services/atproto'; 9 + import { getPublicationFromSlug } from '$lib/config/slugs'; 10 + import type { PublicationPlatform } from '$lib/data/slug-mappings'; 10 11 11 12 /** 12 13 * Smart document redirect handler for slugged publications 13 14 * 14 - * Automatically detects whether the post is from Leaflet or WhiteWind (if enabled) 15 + * Automatically detects whether the post is from Standard.site, Leaflet, or WhiteWind 15 16 * and redirects to the appropriate URL. 16 17 * 17 18 * Priority order: 18 - * 1. Leaflet: Uses publication's base_path or https://leaflet.pub/{DID}/{publicationRkey}/{rkey} 19 - * 2. WhiteWind: https://whtwnd.com/{DID}/{rkey} (only if PUBLIC_ENABLE_WHITEWIND is true) 19 + * 1. Standard.site: Uses publication's URL + document path 20 + * 2. Leaflet: Uses publication's base_path or https://leaflet.pub/{DID}/{publicationRkey}/{rkey} 21 + * 3. WhiteWind: https://whtwnd.com/{DID}/{rkey} (only if PUBLIC_ENABLE_WHITEWIND is true) 20 22 * 21 23 * If detection fails, falls back to PUBLIC_BLOG_FALLBACK_URL or returns 404. 22 24 * 23 - * Uses slug mapping to determine which publication to check. 25 + * Uses slug mapping to determine which publication and platform to check. 24 26 */ 25 27 26 28 async function detectPostPlatform( 27 29 rkey: string, 28 - publicationRkey: string 29 - ): Promise<{ platform: 'whitewind' | 'leaflet' | 'unknown'; url?: string }> { 30 + publicationRkey: string, 31 + platform: PublicationPlatform 32 + ): Promise<{ platform: 'whitewind' | 'leaflet' | 'standard.site' | 'unknown'; url?: string }> { 30 33 try { 31 - // Check Leaflet FIRST (prioritized) using atproto services 34 + // Check based on the platform specified in slug mapping 35 + if (platform === 'standard.site') { 36 + // Check Standard.site documents 37 + const standardRecord = await withFallback( 38 + PUBLIC_ATPROTO_DID, 39 + async (agent) => { 40 + try { 41 + const response = await agent.com.atproto.repo.getRecord({ 42 + repo: PUBLIC_ATPROTO_DID, 43 + collection: 'site.standard.document', 44 + rkey 45 + }); 46 + return response.data; 47 + } catch (err) { 48 + return null; 49 + } 50 + }, 51 + true 52 + ); 53 + 54 + if (standardRecord) { 55 + const value = standardRecord.value as any; 56 + const documentSite = value?.site; 57 + 58 + // Fetch publications to get the publication info 59 + const { publications } = await fetchStandardSitePublications(); 60 + let publication = null; 61 + 62 + // Check if site points to a publication URI 63 + if (documentSite?.startsWith('at://')) { 64 + publication = publications.find((p) => p.uri === documentSite); 65 + 66 + // Verify this document belongs to the requested publication 67 + if (publication && publication.rkey !== publicationRkey) { 68 + return { platform: 'unknown' }; 69 + } 70 + } 71 + 72 + // Build the URL 73 + let url: string; 74 + if (publication) { 75 + const basePath = publication.url.endsWith('/') 76 + ? publication.url.slice(0, -1) 77 + : publication.url; 78 + const docPath = value.path || `/${rkey}`; 79 + url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 80 + } else { 81 + // Use the site value directly (it's a URL) 82 + const basePath = documentSite.endsWith('/') 83 + ? documentSite.slice(0, -1) 84 + : documentSite; 85 + const docPath = value.path || `/${rkey}`; 86 + url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 87 + } 88 + 89 + return { 90 + platform: 'standard.site', 91 + url 92 + }; 93 + } 94 + } 95 + 96 + // Check Leaflet (prioritized for leaflet platform or as fallback) 32 97 const leafletRecord = await withFallback( 33 98 PUBLIC_ATPROTO_DID, 34 99 async (agent) => { ··· 138 203 }); 139 204 } 140 205 141 - // Get the publication rkey from the slug 142 - const publicationRkey = getPublicationRkeyFromSlug(slug); 206 + // Get the publication info from the slug 207 + const publicationInfo = getPublicationFromSlug(slug); 143 208 144 - if (!publicationRkey) { 209 + if (!publicationInfo) { 145 210 return new Response( 146 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, 211 + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 147 212 { 148 213 status: 404, 149 214 headers: { ··· 152 217 } 153 218 ); 154 219 } 220 + 221 + const { rkey: publicationRkey, platform } = publicationInfo; 155 222 156 223 // Validate TID format (AT Protocol record key) 157 224 const tidPattern = /^[a-zA-Z0-9]{12,16}$/; ··· 166 233 } 167 234 168 235 // Detect platform and get appropriate URL 169 - const detection = await detectPostPlatform(rkey, publicationRkey); 236 + const detection = await detectPostPlatform(rkey, publicationRkey, platform); 170 237 171 238 let targetUrl: string | null = null; 172 239 let statusCode = 301; ··· 179 246 targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`; 180 247 } else { 181 248 // No fallback configured, return 404 182 - const publicationNote = `\n\nNote: Only checking Leaflet publication with rkey: ${publicationRkey}`; 249 + const platformName = platform === 'standard.site' ? 'Standard.site' : 'Leaflet'; 250 + const publicationNote = `\n\nNote: Only checking ${platformName} publication with rkey: ${publicationRkey}`; 183 251 const whiteWindNote = 184 252 PUBLIC_ENABLE_WHITEWIND === 'true' ? '\n- WhiteWind: https://whtwnd.com' : ''; 253 + const standardSiteNote = platform === 'standard.site' ? '\n- Standard.site: https://standard.site' : ''; 185 254 186 255 return new Response( 187 256 `Document not found: ${rkey} 188 257 189 - This document could not be found in the Leaflet publication for slug "${slug}"${PUBLIC_ENABLE_WHITEWIND === 'true' ? ' or WhiteWind' : ''}.${publicationNote} 258 + This document could not be found in the ${platformName} publication for slug "${slug}"${PUBLIC_ENABLE_WHITEWIND === 'true' ? ' or WhiteWind' : ''}.${publicationNote} 190 259 191 260 Please check: 192 - - Leaflet: https://leaflet.pub${whiteWindNote}`, 261 + - Leaflet: https://leaflet.pub${standardSiteNote}${whiteWindNote}`, 193 262 { 194 263 status: 404, 195 264 headers: {