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, pronouns, 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 in-memory cache with configurable TTL support for all AT Protocol data
- PDS Resolution: Automatic PDS discovery with fallback to Bluesky public API for maximum reliability
- Standard.site Integration: Full support for Standard.site document storage and display
-
Standard.site Publishing System:
- Store and retrieve documents using the Standard.site protocol
- Multi-publication support via slug mapping
- Intelligent RSS feed generation
- Archive page displaying all your documents
- Full integration with the AT Protocol ecosystem
- Automatic document fetching and caching
-
Flexible Publication Management:
- Map friendly URL slugs to Standard.site publications
- Support for unlimited publications with individual configurations
- Smart redirects to publication URLs
- Publication-filtered RSS feeds
-
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 with HLS.js streaming
-
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 System:
- Server-side Proxy: CORS-free artwork fetching through
/api/artworkendpoint - Cascading Fallback: MusicBrainz β iTunes β Deezer β Last.fm
- MusicBrainz Integration: Cover Art Archive with automatic release search
- Smart Caching: Caches artwork URLs and search results
- AT Protocol Blob Fallback: Uses blob storage when external artwork unavailable
- Server-side Proxy: CORS-free artwork fetching through
- 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)
-
12 Color Themes: Choose from a curated selection of beautiful color themes:
- Neutral: Sage, Monochrome, Slate
- Warm: Ruby, Coral, Sunset, Amber
- Cool: Forest, Teal, Ocean
- Vibrant: Lavender, Rose
- All themes use OKLCH color space for perceptually uniform colors
- System preference detection with manual override
- Persistent theme selection across sessions
-
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
-
Decimal Clock: Unique decimal time display (optional feature)
-
Happy Mac Easter Egg: Hidden surprise for visitors to discover
-
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
-
Archive Page: Browse all your Standard.site documents in one place
- 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
- Video Streaming: HLS.js integration for adaptive video playback
- Configurable Cache TTL: Fine-tune cache durations for different data types
- CORS Support: Flexible cross-origin configuration for API endpoints
For detailed configuration instructions, see the Configuration Guide.
Quick start:
- Copy
.envto.env.localand update with your AT Protocol DID - Configure publication slugs in
src/lib/config/slugs.ts - Update static files (robots.txt, sitemap.xml, favicons)
- Customize themes in
src/lib/config/themes.config.ts(optional) - Run
npm install && npm run dev
# Required: Your AT Protocol DID
PUBLIC_ATPROTO_DID=did:plc:your-did-here
# Optional: Blog fallback URL
PUBLIC_BLOG_FALLBACK_URL=https://example.com/blog
# Optional: Slingshot integration
PUBLIC_LOCAL_SLINGSHOT_URL=http://localhost:3000
PUBLIC_SLINGSHOT_URL=https://slingshot.microcosm.blue
# Site Metadata (for SEO and social sharing)
PUBLIC_SITE_TITLE=Your Site Title
PUBLIC_SITE_DESCRIPTION=Your site description
PUBLIC_SITE_KEYWORDS=your, keywords, here
PUBLIC_SITE_URL=https://yoursite.com
# CORS Configuration (comma-separated origins)
PUBLIC_CORS_ALLOWED_ORIGINS=https://yoursite.com,https://www.yoursite.com
# Optional: Customizable Cache TTL (in seconds)
CACHE_TTL_PROFILE=60
CACHE_TTL_SITE_INFO=120
CACHE_TTL_LINKS=60
CACHE_TTL_MUSIC_STATUS=10
CACHE_TTL_KIBUN_STATUS=15
CACHE_TTL_TANGLED_REPOS=60
CACHE_TTL_BLOG_POSTS=30
CACHE_TTL_PUBLICATIONS=60
CACHE_TTL_INDIVIDUAL_POST=60
CACHE_TTL_IDENTITY=1440- 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
β β β βββ HappyMacEasterEgg.svelte
β β β βββ layout/ # Header, Footer, Navigation
β β β β βββ ColorThemeToggle.svelte
β β β β βββ DecimalClock.svelte
β β β β βββ DecimalClockInfoBox.svelte
β β β β βββ ThemeToggle.svelte
β β β β βββ WolfToggle.svelte
β β β β βββ main/
β β β β βββ card/ # Status cards
β β β β β βββ BlueskyPostCard.svelte
β β β β β βββ KibunStatusCard.svelte
β β β β β βββ LinkCard.svelte
β β β β β βββ MusicStatusCard.svelte
β β β β β βββ PostCard.svelte
β β β β β βββ ProfileCard.svelte
β β β β β βββ TangledRepoCard.svelte
β β β β βββ DynamicLinks.svelte
β β β β βββ ScrollToTop.svelte
β β β βββ seo/ # MetaTags component
β β β βββ ui/ # Reusable UI components
β β β βββ BlogPostCard.svelte
β β β βββ Card.svelte
β β β βββ DocumentCard.svelte
β β β βββ Dropdown.svelte
β β β βββ Pagination.svelte
β β β βββ PostsGroupedView.svelte
β β β βββ SearchBar.svelte
β β β βββ Tabs.svelte
β β βββ config/ # Configuration files
β β β βββ cache.config.ts # Cache TTL settings
β β β βββ slugs.ts # Slug to publication mapping
β β β βββ themes.config.ts # Theme definitions
β β βββ 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
β β β βββ documents.ts # Standard.site documents
β β β βββ engagement.ts # Post engagement (likes/reposts)
β β β βββ fetch.ts # Profile, status, site info, music
β β β βββ media.ts # Blob URL & image handling
β β β βββ musicbrainz.ts # MusicBrainz API integration
β β β βββ pagination/ # Pagination utilities
β β β βββ posts.ts # Blog posts, Bluesky posts
β β β βββ standard.ts # Standard.site integration
β β β βββ types.ts # TypeScript type definitions
β β βββ stores/ # Svelte stores
β β β βββ colorTheme.ts # Color theme management
β β β βββ dropdownState.ts # Dropdown state
β β β βββ happyMac.ts # Happy Mac easter egg
β β β βββ wolfMode.ts # Wolf mode text transformation
β β βββ styles/ # Theme CSS files
β β β βββ themes/ # Individual theme stylesheets
β β βββ utils/ # Utility functions
β βββ routes/ # SvelteKit routes
β β βββ [slug=slug]/ # Dynamic slug-based routes
β β β βββ [rkey]/ # Individual document redirects
β β β βββ atom/ # Deprecated Atom feeds (410 Gone)
β β β βββ rss/ # RSS feed endpoints
β β βββ api/ # API endpoints
β β β βββ artwork/ # Album artwork proxy
β β βββ archive/ # Standard.site documents archive
β β βββ favicon.ico/ # Favicon endpoint
β β βββ 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: Standard.site documents and Bluesky posts
- documents.ts: Standard.site document fetching and management
- standard.ts: Standard.site integration utilities
- 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 with cascading fallbacks
- cache.ts: In-memory caching with configurable TTL support
- pagination/: Utilities for paginated AT Protocol queries
- types.ts: Comprehensive TypeScript definitions for all data structures
import {
fetchProfile,
fetchBlogPosts,
fetchLatestBlueskyPost,
fetchMusicStatus,
fetchKibunStatus,
fetchTangledRepos,
fetchDocuments
} from '$lib/services/atproto';
// Fetch profile data
const profile = await fetchProfile(fetch);
// Fetch blog posts from Standard.site
const { posts } = await fetchBlogPosts(fetch);
// Fetch latest Bluesky post
const post = await fetchLatestBlueskyPost(fetch);
// Fetch current or last played music
const musicStatus = await fetchMusicStatus(fetch);
// Fetch current mood status
const kibunStatus = await fetchKibunStatus(fetch);
// Fetch code repositories
const repos = await fetchTangledRepos(fetch);
// Fetch Standard.site documents
const documents = await fetchDocuments(fetch);The publication system uses friendly URL slugs that map to Standard.site publications with 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' // Standard.site publication rkey
},
{
slug: 'notes', // Access via /notes
publicationRkey: 'xyz123abc'
}
];/{slug}β Redirects to your Standard.site publication homepage/{slug}/{rkey}β Redirects to the specific document on Standard.site/{slug}/rssβ RSS feed for all documents in the publication/{slug}/atomβ Deprecated (returns 410 Gone, use RSS instead)/archiveβ Browse all Standard.site documents across all publications
Generates an RSS 2.0 feed containing all documents from the specified publication:
- Includes title, link, publication date, and description
- Filtered by publication rkey
- Cached for 1 hour for performance
- Returns 404 if publication has no documents
- Visit your Standard.site publication
- The publication rkey is part of the publication's AT Protocol URI
- You can find it in your Standard.site publication settings
- 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 server-side artwork retrieval system with cascading fallbacks:
-
Server-side API Proxy (
/api/artwork)- Solves CORS issues by proxying requests through your server
- Caches artwork URLs to reduce external API calls
- Handles all external API interactions
-
Cascading Artwork Sources:
- MusicBrainz Cover Art Archive (Primary)
- Uses
releaseMbIdfrom music records when available - Automatic search by album name + artist if ID missing
- Free, no API key required
- Uses
- iTunes Search API (Fallback 1)
- Searches by album + artist or track + artist
- Returns high-resolution artwork (600x600)
- Deezer API (Fallback 2)
- Album artwork search
- Multiple quality options (XL, big, medium)
- Last.fm API (Fallback 3)
- Album info with artwork
- Requires album name
- AT Protocol Blob Storage (Final Fallback)
- Uses
artworkfield from records - Proper PDS blob URL construction
- Uses
- MusicBrainz Cover Art Archive (Primary)
-
Smart Caching:
- Caches MusicBrainz search results to avoid repeated lookups
- Caches final artwork URLs
- Configurable TTL for music status
- 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 configurable TTL (default: 2 minutes)
- Automatic status expiry handling
- Prioritizes album art over track art for better accuracy
Set your DID in .env.local to fetch your music status:
PUBLIC_ATPROTO_DID=did:plc:your-did-here
# Optional: Adjust music status cache duration (in seconds)
CACHE_TTL_MUSIC_STATUS=120The card will automatically display your current or last played track with album artwork.
The site features 12 beautiful color themes organized into four categories:
Neutral Themes
- Sage: Calm green-blue
- Monochrome: Pure greyscale
- Slate: Blue-grey (default)
Warm Themes
- Ruby: Bold red
- Coral: Orange-pink
- Sunset: Warm orange
- Amber: Bright yellow
Cool Themes
- Forest: Natural green
- Teal: Blue-green
- Ocean: Deep blue
Vibrant Themes
- Lavender: Soft purple
- Rose: Pink-red
- OKLCH Color Space: Perceptually uniform colors for consistent brightness
- System Detection: Automatically detects light/dark mode preference
- Persistent Selection: Theme choice saved across sessions
- Smooth Transitions: Animated color changes
- Accessible: All themes meet WCAG contrast requirements
Edit src/lib/config/themes.config.ts to add or modify themes:
export const THEMES: readonly ThemeDefinition[] = [
{
value: 'mytheme',
label: 'My Theme',
description: 'Custom colors',
color: 'oklch(80% 0.2 180)',
category: 'cool'
}
// ... more themes
];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 with cascading fallbacks
# 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
# 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-vercel optimized for Vercel deployment:
- Push your repository to GitHub/GitLab/Bitbucket
- Import project in Vercel
- Add environment variables from
.env.local - Deploy
To use a different platform, change the adapter in svelte.config.js:
import adapter from '@sveltejs/adapter-auto'; // or adapter-node, adapter-static, etc.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.
Store and display documents using the Standard.site protocol.
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 with plugins for Svelte and Tailwind
- svelte-check β Svelte-specific linting
- Svelte 5 Runes β Modern reactivity with better performance
- Framework: SvelteKit 2.50+ with Svelte 5
- Styling: Tailwind CSS 4 with typography plugin
- AT Protocol: @atproto/api v0.18.1
- Video: HLS.js for adaptive streaming
- Icons: @lucide/svelte
- Build Tool: Vite 7
- TypeScript: v5.9+
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.
- 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:...');Edit cache durations in .env.local:
# Profile data (default: 60 seconds)
CACHE_TTL_PROFILE=300
# Music status (default: 120 seconds)
CACHE_TTL_MUSIC_STATUS=60
# Kibun status (default: 120 seconds)
CACHE_TTL_KIBUN_STATUS=90If your music status doesn't show album artwork:
- Ensure your scrobbler includes
releaseMbIdin records (best option) - The system will automatically search MusicBrainz if IDs are missing
- Album name + artist name provides better results than track name
- Check browser console for artwork search results
- Fallback to AT Protocol blob storage if external sources fail
- Icon placeholder displays if no artwork is found
The cascading fallback system tries multiple sources:
- MusicBrainz (with automatic search)
- iTunes
- Deezer
- Last.fm
- AT Protocol blob storage
- Verify
PUBLIC_ATPROTO_DIDis correct - Check slug mapping in
src/lib/config/slugs.ts - Ensure publication rkey matches your Leaflet publication
- Check browser console for AT Protocol service errors
- Verify your Standard.site publications are properly configured
- For Standard.site documents, check the
/archivepage
- Ensure JavaScript is enabled
- Check browser console for errors
- Wolf mode preserves navigation and interactive elements
- Numbers and abbreviations are preserved intentionally
- Toggle is located in the header navigation
- Check browser localStorage is enabled
- Clear site data and try again
- Verify the theme value is valid in
themes.config.ts - Check console for theme-related errors
- 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
The artwork system uses a server-side proxy to avoid CORS issues:
- Ensure the
/api/artworkendpoint is accessible - Check
PUBLIC_CORS_ALLOWED_ORIGINSincludes your domain - Verify external APIs (MusicBrainz, iTunes, etc.) are accessible
- Check server logs for API errors
If you see "Cannot use relative URL with global fetch":
- Ensure all data fetching functions receive the
fetchparameter - Pass
fetchfromloadfunctions to service functions - Use
event.fetchin server-side code - This was fixed in the latest version
- Thanks to the AT Protocol team for creating an open, decentralized protocol
- Thanks to the Bluesky, Standard.site, teal.fm, kibun.social, Tangled, and Linkat teams
- Thanks to MusicBrainz, iTunes, Deezer, and Last.fm for providing free artwork APIs
- Thanks to the Cover Art Archive for hosting album artwork
- 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
Version: 10.7.1