my website at ewancroft.uk

AT Protocol Personal Website#

A modern, feature-rich personal website powered by AT Protocol, built with SvelteKit 2 and Tailwind CSS 4.

Note: This repository contains the source code for Ewan's Corner. The current configuration (environment variables, slug mappings, static files) is specific to that website, but the codebase is designed to be easily adapted for your own AT Protocol-powered site. See Configuration Guide for detailed setup instructions.

🌟 Features#

Core AT Protocol Integration#

  • Dynamic Profile Display: Automatically fetch and display your Bluesky profile information with avatar, banner, follower counts, and bio
  • Site Metadata: Store and display comprehensive site information using the uk.ewancroft.site.info lexicon (credits, tech stack, privacy statement, licenses)
  • Smart Caching: Intelligent 5-minute in-memory cache with TTL support for all AT Protocol data
  • PDS Resolution: Automatic PDS discovery with fallback to Bluesky public API for maximum reliability

Content & Publishing#

  • Multi-Platform Blog System:

    • Leaflet (pub.leaflet.document) - Primary platform with custom domain support
    • WhiteWind (com.whtwnd.blog.entry) - Optional secondary platform (disabled by default)
    • Intelligent RSS feed generation with full content support
    • Automatic draft filtering and non-public post handling
    • Multi-publication support via slug mapping
  • Flexible Publication Management:

    • Map friendly URL slugs to AT Protocol publications
    • Support for unlimited publications with individual configurations
    • Custom base paths for each publication
    • Smart redirects with platform prioritization
    • Intelligent fallback handling for missing content
  • Bluesky Post Display:

    • Showcase latest non-reply posts with rich media support
    • Full thread context with recursive parent fetching
    • Quoted post embedding with media preservation
    • Image galleries with alt text support
    • External link cards with preview generation
    • Video embed support
  • Engagement Tracking:

    • Real-time like and repost counts via Constellation API
    • Paginated engagement data fetching
    • Cached engagement metrics for performance

Music Integration (via teal.fm)#

  • Now Playing Display: Show currently playing or recently played tracks via fm.teal.alpha.actor.status
  • Play History: Display listening history via fm.teal.alpha.feed.play
  • Album Artwork:
    • Primary: MusicBrainz Cover Art Archive integration (no API key required!)
    • Automatic Search: Searches MusicBrainz when release IDs are missing
    • Smart Caching: Caches MusicBrainz lookups to avoid repeated searches
    • Fallback: AT Protocol blob storage for custom artwork
  • Rich Metadata: Artist names, album info, duration, and relative timestamps
  • Multi-Service Support: Works with Last.fm, Spotify, and other scrobbling services
  • Intelligent Expiry: Automatically handles expired "now playing" status

Mood Status (via kibun.social)#

  • Current Mood Display: Show your current mood/feeling via social.kibun.status
  • Emoji Support: Display expressive emoji alongside mood text
  • Relative Timestamps: Show when the mood was last updated
  • Real-time Updates: Automatically refreshes to show your latest status
  • Clean Design: Simple, elegant card that fits seamlessly with other status cards

Developer Tools#

  • Tangled Repository Display: Showcase your code repositories using the sh.tangled.repo lexicon
  • Repository Cards: Display with descriptions, creation dates, labels, and source links
  • Automatic Sorting: Repos sorted by creation date (newest first)

User Experience#

  • Link Board: Display curated link collections from Linkat (blue.linkat.board) with emoji icons
  • Dark Mode: Seamless light/dark theme switching with system preference detection
  • Wolf Mode: Fun "wolf speak" text transformation toggle that converts text to wolf sounds while preserving:
    • Numbers and abbreviations (1K, 2M, 30s, etc.)
    • Capitalization patterns (UPPERCASE → AWOO, Capitalized → Awoo)
    • Punctuation and formatting
    • Navigation and interactive elements
  • Scroll to Top: Smooth scroll-to-top button for long pages
  • Responsive Design: Mobile-first layout that adapts to all screen sizes
  • SEO Optimization: Comprehensive meta tags, Open Graph, and Twitter Card support
  • RSS/Atom Feeds: Multiple feed endpoints for blog posts and status updates

Technical Features#

  • Type-Safe Development: Full TypeScript support with comprehensive type definitions
  • Smart Error Handling: Graceful degradation with informative error states
  • Loading States: Skeleton loaders for all async content
  • Image Optimization: Lazy loading and responsive image handling
  • Blob URL Construction: Proper PDS blob URL generation for media assets
  • Media Extraction: Automatic CID extraction from various image object formats
  • Facet Processing: Rich text with link detection and mention highlighting

📋 Configuration#

For detailed configuration instructions, see the Configuration Guide.

Quick start:

  1. Copy .env.example to .env.local and add your AT Protocol DID
  2. Configure publication slugs in src/lib/config/slugs.ts
  3. Update static files (robots.txt, sitemap.xml, favicons)
  4. Run npm install && npm run dev

🚀 Getting Started#

Prerequisites#

  • Node.js 18+ and npm
  • An AT Protocol DID (Decentralized Identifier) from Bluesky

Installation#

  1. Clone the repository:

    git clone git@github.com:ewanc26/website.git
    cd website
    
  2. Install dependencies:

    npm install
    
  3. Configure environment variables:

    cp .env .env.local
    

    Edit .env.local with your settings (see Configuration Guide for details)

  4. Configure publication slugs in src/lib/config/slugs.ts

  5. Start the development server:

    npm run dev
    

    Visit http://localhost:5173 to view your site

📁 Project Structure#

website/
├── src/
│   ├── lib/
│   │   ├── assets/               # Static assets (images, icons)
│   │   ├── components/           # Reusable Svelte components
│   │   │   ├── layout/          # Header, Footer, Navigation, ThemeToggle, WolfToggle
│   │   │   │   └── main/
│   │   │   │       ├── card/    # ProfileCard, MusicStatusCard, etc.
│   │   │   │       ├── DynamicLinks.svelte
│   │   │   │       ├── ScrollToTop.svelte
│   │   │   │       └── TangledRepos.svelte
│   │   │   ├── seo/             # MetaTags component
│   │   │   └── ui/              # Reusable UI components (Card, etc.)
│   │   ├── config/              # Configuration files
│   │   │   └── slugs.ts         # Slug to publication mapping
│   │   ├── data/                # Static data (navigation items)
│   │   ├── helper/              # Helper functions (meta tags, OG images)
│   │   ├── services/            # External service integrations
│   │   │   └── atproto/         # AT Protocol service layer
│   │   │       ├── agents.ts    # Agent management & PDS resolution
│   │   │       ├── cache.ts     # In-memory caching
│   │   │       ├── engagement.ts # Post engagement (likes/reposts)
│   │   │       ├── fetch.ts     # Profile, status, site info, music status
│   │   │       ├── media.ts     # Blob URL & image handling
│   │   │       ├── musicbrainz.ts # MusicBrainz API integration
│   │   │       ├── posts.ts     # Blog posts, Bluesky posts, publications
│   │   │       ├── tangled.ts   # Tangled repository fetching
│   │   │       └── types.ts     # TypeScript type definitions
│   │   ├── stores/              # Svelte stores
│   │   │   └── wolfMode.ts      # Wolf mode text transformation
│   │   └── utils/               # Utility functions (date formatting, etc.)
│   ├── routes/                  # SvelteKit routes
│   │   ├── [slug=slug]/         # Dynamic slug-based publication routes
│   │   │   ├── [rkey]/          # Individual document redirects
│   │   │   ├── atom/            # Deprecated Atom feeds (410 Gone)
│   │   │   └── rss/             # RSS feed endpoints
│   │   ├── favicon.ico/         # Favicon endpoint
│   │   ├── now/                 # Status feed endpoints
│   │   │   ├── atom/            # Deprecated Atom feeds
│   │   │   └── rss/             # RSS feeds
│   │   └── site/
│   │       └── meta/            # Site metadata page
│   ├── app.css                  # Global styles
│   └── app.html                 # HTML template
├── static/                      # Static files (favicon, robots.txt, etc.)
└── package.json

🔧 AT Protocol Services#

The application includes a comprehensive AT Protocol service layer in src/lib/services/atproto/:

Core Services#

  • agents.ts: Agent management with automatic PDS resolution and fallback to the Bluesky public API
  • fetch.ts: Profile, status, site info, links, and music status fetching
  • posts.ts: Blog posts (WhiteWind & Leaflet), Bluesky posts, and publications
  • tangled.ts: Repository information from Tangled lexicon
  • engagement.ts: Post engagement data (likes/reposts) via Constellation API
  • media.ts: Image and blob URL handling with CID extraction
  • musicbrainz.ts: MusicBrainz API integration for album artwork
  • cache.ts: In-memory caching with configurable TTL support
  • types.ts: Comprehensive TypeScript definitions for all data structures

Usage Examples#

import {
  fetchProfile,
  fetchBlogPosts,
  fetchLatestBlueskyPost,
  fetchMusicStatus,
  fetchTangledRepos
} from '$lib/services/atproto';

// Fetch profile data
const profile = await fetchProfile();

// Fetch blog posts from WhiteWind and/or Leaflet
const { posts } = await fetchBlogPosts();

// Fetch latest Bluesky post
const post = await fetchLatestBlueskyPost();

// Fetch current or last played music
const musicStatus = await fetchMusicStatus();

// Fetch code repositories
const repos = await fetchTangledRepos();

📝 Publication System#

The publication system uses friendly URL slugs that map to Leaflet publications, with support for multiple platforms and intelligent URL redirects.

Slug Configuration#

Publications are mapped to URL slugs in src/lib/config/slugs.ts:

export const slugMappings: SlugMapping[] = [
  {
    slug: 'blog', // Access via /blog
    publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey
  },
  {
    slug: 'notes', // Access via /notes
    publicationRkey: 'xyz123abc'
  }
];

Supported Platforms#

  1. Leaflet (pub.leaflet.document) – Prioritized by default

    • Format: Custom domain or https://leaflet.pub/lish/{did}/{publication}/{rkey}
    • Supports multiple publications via slug mapping
    • Respects base_path configuration
    • Always checked first
  2. WhiteWind (com.whtwnd.blog.entry) – Optional, disabled by default

    • Format: https://whtwnd.com/{did}/{rkey}
    • Automatically filters out drafts and non-public posts
    • Only checked if PUBLIC_ENABLE_WHITEWIND=true

Publication Routes#

  • /{slug} – Redirects to your publication homepage (configured in slugs.ts)
  • /{slug}/{rkey} – Smart redirect to the correct platform (checks Leaflet first, then WhiteWind if enabled)
  • /{slug}/rss – Intelligent RSS feed (redirects to Leaflet RSS by default, or generates WhiteWind RSS if enabled)
  • /{slug}/atom – Deprecated (returns 410 Gone, use RSS instead)

Priority Order#

  1. Leaflet is always checked first for publications and documents
  2. The slug mapping determines which publication to check
  3. WhiteWind is only checked if PUBLIC_ENABLE_WHITEWIND=true
  4. If neither platform has the document, it falls back to PUBLIC_BLOG_FALLBACK_URL if configured
  5. Returns 404 if the document isn't found and no fallback is set

RSS Feed Behavior#

  • WhiteWind disabled (default): Redirects to Leaflet's native RSS feed (includes full content)
  • WhiteWind enabled with posts: Generates an RSS feed with WhiteWind post links
  • No posts found: Returns 404

Finding Your Publication Rkey#

  1. Visit your Leaflet publication page
  2. The URL will be in the format: https://leaflet.pub/lish/{did}/{rkey}
  3. Copy the {rkey} part (e.g., 3m3x4bgbsh22k)
  4. Add it to your slug mapping in src/lib/config/slugs.ts

🎵 Music Integration#

The site displays your music listening activity via teal.fm integration:

Supported Record Types#

  • fm.teal.alpha.actor.status: Current "Now Playing" status with expiry
  • fm.teal.alpha.feed.play: Historical play records

Album Artwork System#

The music card uses a sophisticated artwork retrieval system:

  1. MusicBrainz Cover Art Archive (Primary)

    • Uses releaseMbId from music records
    • Free, no API key required
    • Automatic search fallback when IDs are missing
    • Caches search results to avoid repeated lookups
  2. AT Protocol Blob Storage (Fallback)

    • Uses artwork field from records
    • Proper PDS blob URL construction

Features#

  • Displays track name, artists, album, and duration
  • Shows relative timestamps ("2 minutes ago")
  • Links to origin URLs (Last.fm, Spotify, etc.)
  • Responsive artwork display with fallback icons
  • Smart caching with 5-minute TTL
  • Automatic status expiry handling

Configuration#

Set your DID in .env.local to fetch your music status:

PUBLIC_ATPROTO_DID=did:plc:your-did-here

The card will automatically display your current or last played track.

🔐 CORS Configuration#

The API endpoints support Cross-Origin Resource Sharing (CORS) via dynamic configuration:

Environment Variable#

# Single origin
PUBLIC_CORS_ALLOWED_ORIGINS="https://example.com"

# Multiple origins (comma-separated)
PUBLIC_CORS_ALLOWED_ORIGINS="https://example.com,https://app.example.com,https://www.example.com"

# Allow all origins (not recommended for production)
PUBLIC_CORS_ALLOWED_ORIGINS="*"

How It Works#

  1. Dynamic Origin Matching: The server checks the Origin header against the allowed list
  2. Preflight Requests: OPTIONS requests are handled automatically with proper CORS headers
  3. Security: Only specified origins receive CORS headers (unless using *)
  4. Headers Set:
    • Access-Control-Allow-Origin: The requesting origin (if allowed)
    • Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
    • Access-Control-Allow-Headers: Content-Type, Authorization
    • Access-Control-Max-Age: 86400 (24 hours)

API Endpoints#

CORS is automatically applied to all routes under /api/:

  • /api/artwork - Album artwork fetching service

Testing CORS#

# Test from command line
curl -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -X OPTIONS \
  http://localhost:5173/api/artwork

# Check response headers for:
# Access-Control-Allow-Origin: https://example.com

Security Recommendations#

  1. Production: Specify exact allowed origins instead of using *
  2. Development: Use * or localhost origins for testing
  3. Multiple Domains: List all your domains that need API access
  4. HTTPS Only: Always use HTTPS origins in production

🎨 Styling#

The project uses:

  • Tailwind CSS 4: Latest Tailwind with new features and improved performance
  • @tailwindcss/typography: Beautiful prose styling for blog content
  • @tailwindcss/vite: Vite plugin for optimal Tailwind integration
  • Custom Color Palette: Semantic color tokens (canvas, ink, primary) for consistent theming
  • Dark Mode: System preference detection with manual override
  • Responsive Design: Mobile-first approach with breakpoint utilities

🏗️ Building for Production#

# Build the application
npm run build

# Preview the production build
npm run preview

The build output will be in the .svelte-kit directory, ready for deployment.

📦 Deployment#

This project uses @sveltejs/adapter-auto, which automatically selects the best adapter for your deployment platform:

  • Vercel: Automatic detection and optimization
  • Netlify: Automatic detection and optimization
  • Cloudflare Pages: Automatic detection and optimization
  • Node.js: Fallback option

For other platforms, see the SvelteKit adapters documentation.

🔍 Custom Lexicons#

The site supports several custom AT Protocol lexicons:

Site Information (uk.ewancroft.site.info)#

Store comprehensive site metadata:

  • Technology stack
  • Privacy statement
  • Open-source information
  • Credits and licenses
  • Related services

Display a collection of links with emoji icons.

Music Status (fm.teal.alpha.actor.status & fm.teal.alpha.feed.play)#

Show music listening activity via teal.fm integration.

Mood Status (social.kibun.status)#

Display your current mood or feeling via kibun.social integration.

Tangled Repositories (sh.tangled.repo)#

Display code repositories with descriptions, labels, and metadata.

🛠️ Development#

Available Scripts#

  • npm run dev – Start the development server
  • npm run build – Build for production
  • npm run preview – Preview the production build
  • npm run check – Type-check the project
  • npm run check:watch – Type-check in watch mode
  • npm run format – Format code with Prettier
  • npm run lint – Check code formatting

Code Quality#

The project uses:

  • TypeScript – Full type safety throughout
  • Prettier – Consistent code formatting
  • svelte-check – Svelte-specific linting
  • Svelte 5 Runes – Modern reactivity with better performance

🤝 Contributing#

Contributions are welcome! Please feel free to submit a pull request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License#

This project is open-source. See the LICENSE file for more details on the website source code specifically and the THIRD-PARTY-LICENSES.txt file for third-party dependencies.

💡 Tips & Troubleshooting#

Finding Your DID#

  1. Visit PDSls
  2. Enter your handle (e.g., ewancroft.uk)
  3. Look for the did:plc (or did:web) in the Repository field
  4. If not visible, click the arrow to the right of the text

Cache Management#

The AT Protocol services use an in-memory cache with configurable TTL:

import { cache } from '$lib/services/atproto';

// Clear all cache
cache.clear();

// Clear a specific entry
cache.delete('profile:did:plc:...');

// Get cache statistics
const profile = cache.get<ProfileData>('profile:did:plc:...');

Music Status Not Showing Artwork#

If your music status doesn't show album artwork:

  1. Ensure your scrobbler (e.g., piper) is including releaseMbId in records
  2. The system will automatically search MusicBrainz if IDs are missing
  3. Check browser console for MusicBrainz search results
  4. Fallback to blob storage if available
  5. Icon placeholder displays if no artwork is found

Documents Not Found#

  1. Verify PUBLIC_ATPROTO_DID is correct
  2. Check slug mapping in src/lib/config/slugs.ts
  3. Ensure publication rkey matches your Leaflet publication
  4. Verify documents are published (not drafts)
  5. If using WhiteWind, ensure PUBLIC_ENABLE_WHITEWIND=true
  6. Check browser console for AT Protocol service errors

Wolf Mode Not Working#

  1. Ensure JavaScript is enabled
  2. Check browser console for errors
  3. Wolf mode preserves navigation and interactive elements
  4. Numbers and abbreviations are preserved intentionally

Build Errors#

  1. Clear .svelte-kit directory: rm -rf .svelte-kit
  2. Remove node_modules: rm -rf node_modules
  3. Clear package lock: rm package-lock.json
  4. Reinstall: npm install
  5. Try building: npm run build

🙏 Acknowledgements#

  • Thanks to the AT Protocol team for creating an open, decentralized protocol
  • Thanks to the Bluesky, WhiteWind, Leaflet, teal.fm, kibun.social, Tangled, and Linkat teams
  • Thanks to MusicBrainz for providing free album artwork via the Cover Art Archive
  • Inspired by the personal-web movement and IndieWeb principles
  • Built with love using modern web technologies

Built with ❤️ using SvelteKit, AT Protocol, and open-source tools