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.
- 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.infolexicon (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
-
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
- Leaflet (
-
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
- 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
- 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
- Tangled Repository Display: Showcase your code repositories using the
sh.tangled.repolexicon - Repository Cards: Display with descriptions, creation dates, labels, and source links
- Automatic Sorting: Repos sorted by creation date (newest first)
- 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
- 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
For detailed configuration instructions, see the Configuration Guide.
Quick start:
- Copy
.env.exampleto.env.localand add your AT Protocol DID - Configure publication slugs in
src/lib/config/slugs.ts - Update static files (robots.txt, sitemap.xml, favicons)
- Run
npm install && npm run dev
- Node.js 18+ and npm
- An AT Protocol DID (Decentralized Identifier) from Bluesky
-
Clone the repository:
git clone git@github.com:ewanc26/website.git cd website -
Install dependencies:
npm install
-
Configure environment variables:
cp .env .env.local
Edit
.env.localwith your settings (see Configuration Guide for details) -
Configure publication slugs in
src/lib/config/slugs.ts -
Start the development server:
npm run dev
Visit
http://localhost:5173to view your site
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
The application includes a comprehensive AT Protocol service layer in src/lib/services/atproto/:
- 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
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();The publication system uses friendly URL slugs that map to Leaflet publications, with support for multiple platforms and intelligent URL redirects.
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'
}
];-
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_pathconfiguration - Always checked first
- Format: Custom domain or
-
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
- Format:
/{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)
- Leaflet is always checked first for publications and documents
- The slug mapping determines which publication to check
- WhiteWind is only checked if
PUBLIC_ENABLE_WHITEWIND=true - If neither platform has the document, it falls back to
PUBLIC_BLOG_FALLBACK_URLif configured - Returns 404 if the document isn't found and no fallback is set
- 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
- Visit your Leaflet publication page
- The URL will be in the format:
https://leaflet.pub/lish/{did}/{rkey} - Copy the
{rkey}part (e.g.,3m3x4bgbsh22k) - Add it to your slug mapping in
src/lib/config/slugs.ts
The site displays your music listening activity via teal.fm integration:
fm.teal.alpha.actor.status: Current "Now Playing" status with expiryfm.teal.alpha.feed.play: Historical play records
The music card uses a sophisticated artwork retrieval system:
-
MusicBrainz Cover Art Archive (Primary)
- Uses
releaseMbIdfrom music records - Free, no API key required
- Automatic search fallback when IDs are missing
- Caches search results to avoid repeated lookups
- Uses
-
AT Protocol Blob Storage (Fallback)
- Uses
artworkfield from records - Proper PDS blob URL construction
- Uses
- 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
Set your DID in .env.local to fetch your music status:
PUBLIC_ATPROTO_DID=did:plc:your-did-hereThe card will automatically display your current or last played track.
The API endpoints support Cross-Origin Resource Sharing (CORS) via dynamic configuration:
# 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="*"- Dynamic Origin Matching: The server checks the
Originheader against the allowed list - Preflight Requests: OPTIONS requests are handled automatically with proper CORS headers
- Security: Only specified origins receive CORS headers (unless using
*) - Headers Set:
Access-Control-Allow-Origin: The requesting origin (if allowed)Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Max-Age: 86400 (24 hours)
CORS is automatically applied to all routes under /api/:
/api/artwork- Album artwork fetching service
# 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- Production: Specify exact allowed origins instead of using
* - Development: Use
*or localhost origins for testing - Multiple Domains: List all your domains that need API access
- HTTPS Only: Always use HTTPS origins in production
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
# Build the application
npm run build
# Preview the production build
npm run previewThe build output will be in the .svelte-kit directory, ready for 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.
The site supports several custom AT Protocol lexicons:
Store comprehensive site metadata:
- Technology stack
- Privacy statement
- Open-source information
- Credits and licenses
- Related services
Display a collection of links with emoji icons.
Show music listening activity via teal.fm integration.
Display your current mood or feeling via kibun.social integration.
Display code repositories with descriptions, labels, and metadata.
npm run devβ Start the development servernpm run buildβ Build for productionnpm run previewβ Preview the production buildnpm run checkβ Type-check the projectnpm run check:watchβ Type-check in watch modenpm run formatβ Format code with Prettiernpm run lintβ Check code formatting
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
Contributions are welcome! Please feel free to submit a pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
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.
- AT Protocol Documentation
- SvelteKit Documentation
- Tailwind CSS Documentation
- Bluesky
- WhiteWind
- Leaflet
- teal.fm
- kibun.social
- MusicBrainz
- Tangled
- Linkat
- Visit PDSls
- Enter your handle (e.g.,
ewancroft.uk) - Look for the
did:plc(ordid:web) in the Repository field - If not visible, click the arrow to the right of the text
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:...');If your music status doesn't show album artwork:
- Ensure your scrobbler (e.g., piper) is including
releaseMbIdin records - The system will automatically search MusicBrainz if IDs are missing
- Check browser console for MusicBrainz search results
- Fallback to blob storage if available
- Icon placeholder displays if no artwork is found
- Verify
PUBLIC_ATPROTO_DIDis correct - Check slug mapping in
src/lib/config/slugs.ts - Ensure publication rkey matches your Leaflet publication
- Verify documents are published (not drafts)
- If using WhiteWind, ensure
PUBLIC_ENABLE_WHITEWIND=true - Check browser console for AT Protocol service errors
- Ensure JavaScript is enabled
- Check browser console for errors
- Wolf mode preserves navigation and interactive elements
- Numbers and abbreviations are preserved intentionally
- Clear
.svelte-kitdirectory:rm -rf .svelte-kit - Remove
node_modules:rm -rf node_modules - Clear package lock:
rm package-lock.json - Reinstall:
npm install - Try building:
npm run build
- 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