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, 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
Content & Publishing#
-
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
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 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
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.repolexicon - Repository Cards: Display with descriptions, creation dates, labels, and source links
- Automatic Sorting: Repos sorted by creation date (newest first)
User Experience#
-
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
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
- 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
📋 Configuration#
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
Environment Variables#
# 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
🚀 Getting Started#
Prerequisites#
- Node.js 18+ and npm
- An AT Protocol DID (Decentralized Identifier) from Bluesky
Installation#
-
Clone the repository:
git clone git@github.com:ewanc26/website.git cd website -
Install dependencies:
npm install -
Configure environment variables:
cp .env .env.localEdit
.env.localwith your settings (see Configuration Guide for details) -
Configure publication slugs in
src/lib/config/slugs.ts -
Start the development server:
npm run devVisit
http://localhost:5173to view your site
📁 Project Structure#
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
🔧 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: 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
Usage Examples#
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);
📝 Publication System#
The publication system uses friendly URL slugs that map to Standard.site publications with 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' // Standard.site publication rkey
},
{
slug: 'notes', // Access via /notes
publicationRkey: 'xyz123abc'
}
];
Publication Routes#
/{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
RSS Feed Behavior#
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
Finding Your Publication Rkey#
- 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
🎵 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 expiryfm.teal.alpha.feed.play: Historical play records
Album Artwork System#
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
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 configurable TTL (default: 2 minutes)
- Automatic status expiry handling
- Prioritizes album art over track art for better accuracy
Configuration#
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=120
The card will automatically display your current or last played track with album artwork.
🎨 Theme System#
The site features 12 beautiful color themes organized into four categories:
Available Themes#
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
Theme Features#
- 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
Customizing Themes#
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
];
🔐 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#
- 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)
API Endpoints#
CORS is automatically applied to all routes under /api/:
/api/artwork- Album artwork fetching service with cascading fallbacks
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#
- 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
🏗️ 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-vercel optimized for Vercel deployment:
Vercel (Recommended)#
- Push your repository to GitHub/GitLab/Bitbucket
- Import project in Vercel
- Add environment variables from
.env.local - Deploy
Other Platforms#
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.
🔍 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
Link Board (blue.linkat.board)#
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.
Standard.site Documents#
Store and display documents using the Standard.site protocol.
🛠️ Development#
Available Scripts#
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
Code Quality#
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
Tech Stack#
- 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+
🤝 Contributing#
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
📄 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.
🔗 Links#
💡 Tips & Troubleshooting#
Finding Your DID#
- 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
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:...');
Customizing Cache TTL#
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=90
Music Status Not Showing Artwork#
If 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
Documents Not Found#
- 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
Wolf Mode Not Working#
- 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
Theme Not Persisting#
- 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
Build 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
CORS Issues with Artwork#
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
SvelteKit Fetch Error#
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
🙏 Acknowledgements#
- 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