···11# AT Protocol Personal Website
2233-A modern, AT Protocol-powered personal website template built with SvelteKit 2 and Tailwind CSS 4.
33+A modern, feature-rich personal website powered by AT Protocol, built with SvelteKit 2 and Tailwind CSS 4.
4455> **Note**: This repository contains the source code for [Ewan's Corner](https://ewancroft.uk). 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 the [Configuration](#configuration) section below for details on personalising it for your use.
6677## 🌟 Features
8899-- **AT Protocol Integration**: Fetch and display content from your AT Protocol repository
1010-- **Multi-Publication Support**: Map friendly URL slugs to Leaflet publications with a simple config
1111-- **Multi-Platform Blog**: Seamlessly aggregate blog posts from WhiteWind and/or Leaflet (configurable)
1212-- **Dynamic Profile**: Automatically display your Bluesky profile information
1313-- **Custom Status**: Show real-time status updates using custom AT Protocol lexicons
1414-- **Link Board**: Display a Linkat board with emoji-styled link cards
1515-- **Bluesky Posts**: Showcase your latest non-reply Bluesky posts with rich media support
1616-- **Smart Redirects**: Intelligent redirection system for publication URLs with platform prioritisation
1717-- **Responsive Design**: Mobile-first layout with dark-mode support
1818-- **RSS Feed**: Intelligent RSS-feed handling for WhiteWind and/or Leaflet posts
1919-- **Type-Safe**: Full TypeScript support throughout the application
99+### Core AT Protocol Integration
20102121-## Configuration
1111+- **Dynamic Profile Display**: Automatically fetch and display your Bluesky profile information with avatar, banner, follower counts, and bio
1212+- **Site Metadata**: Store and display comprehensive site information using the `uk.ewancroft.site.info` lexicon (credits, tech stack, privacy statement, licenses)
1313+- **Smart Caching**: Intelligent 5-minute in-memory cache with TTL support for all AT Protocol data
1414+- **PDS Resolution**: Automatic PDS discovery with fallback to Bluesky public API for maximum reliability
22152323-Before using this template, you'll need to update several configuration files with your own information:
1616+### Content & Publishing
24172525-### Environment Variables (`.env`)
1818+- **Multi-Platform Blog System**:
1919+ - **Leaflet** (`pub.leaflet.document`) - Primary platform with custom domain support
2020+ - **WhiteWind** (`com.whtwnd.blog.entry`) - Optional secondary platform (disabled by default)
2121+ - Intelligent RSS feed generation with full content support
2222+ - Automatic draft filtering and non-public post handling
2323+ - Multi-publication support via slug mapping
26242727-Update these with your own values:
2525+- **Flexible Publication Management**:
2626+ - Map friendly URL slugs to AT Protocol publications
2727+ - Support for unlimited publications with individual configurations
2828+ - Custom base paths for each publication
2929+ - Smart redirects with platform prioritization
3030+ - Intelligent fallback handling for missing content
28312929-```ini
3030-PUBLIC_ATPROTO_DID=did:plc:your-did-here # Your AT Protocol DID
3131-PUBLIC_SITE_TITLE="Your Site Title" # Your site name
3232-PUBLIC_SITE_DESCRIPTION="..." # Your description
3333-PUBLIC_SITE_URL="https://example.com" # Your domain
3434-````
3535-3636-### Publication Slug Mappings (`src/lib/data/slug-mappings.ts`)
3737-3838-Replace the example mappings with your own Leaflet publication rkeys:
3939-4040-```typescript
4141-export const slugMappings: SlugMapping[] = [
4242- { slug: 'blog', publicationRkey: 'your-rkey-here' }
4343-];
4444-```
4545-4646-### Static Files
4747-4848-Update or remove these files that are specific to the example site:
4949-5050-- `static/robots.txt` - Update the sitemap URL
5151-- `static/sitemap.xml` - Update with your domain and pages
5252-- `static/.well-known/*` - These files are specific to ewancroft.uk and should be replaced with your own
5353-5454-### Favicon and Assets
3232+- **Bluesky Post Display**:
3333+ - Showcase latest non-reply posts with rich media support
3434+ - Full thread context with recursive parent fetching
3535+ - Quoted post embedding with media preservation
3636+ - Image galleries with alt text support
3737+ - External link cards with preview generation
3838+ - Video embed support
55395656-Replace the favicons in `static/favicon/` with your own branding.
4040+- **Engagement Tracking**:
4141+ - Real-time like and repost counts via Constellation API
4242+ - Paginated engagement data fetching
4343+ - Cached engagement metrics for performance
57445858-## 🚀 Getting Started
4545+### Music Integration (via teal.fm)
59466060-### Prerequisites
4747+- **Now Playing Display**: Show currently playing or recently played tracks via `fm.teal.alpha.actor.status`
4848+- **Play History**: Display listening history via `fm.teal.alpha.feed.play`
4949+- **Album Artwork**:
5050+ - **Primary**: MusicBrainz Cover Art Archive integration (no API key required!)
5151+ - **Automatic Search**: Searches MusicBrainz when release IDs are missing
5252+ - **Smart Caching**: Caches MusicBrainz lookups to avoid repeated searches
5353+ - **Fallback**: AT Protocol blob storage for custom artwork
5454+- **Rich Metadata**: Artist names, album info, duration, and relative timestamps
5555+- **Multi-Service Support**: Works with Last.fm, Spotify, and other scrobbling services
5656+- **Intelligent Expiry**: Automatically handles expired "now playing" status
61576262-- Node.js 18+ and npm
6363-- An AT Protocol DID (Decentralised Identifier) from Bluesky
5858+### Developer Tools
64596565-### Installation
6060+- **Tangled Repository Display**: Showcase your code repositories using the `sh.tangled.repo` lexicon
6161+- **Repository Cards**: Display with descriptions, creation dates, labels, and source links
6262+- **Automatic Sorting**: Repos sorted by creation date (newest first)
66636767-1. Clone the repository:
6464+### User Experience
68656969-```bash
7070-git clone git@github.com:ewanc26/website.git
7171-cd website-redesign
7272-```
6666+- **Link Board**: Display curated link collections from Linkat (`blue.linkat.board`) with emoji icons
6767+- **Dark Mode**: Seamless light/dark theme switching with system preference detection
6868+- **Wolf Mode**: Fun "wolf speak" text transformation toggle that converts text to wolf sounds while preserving:
6969+ - Numbers and abbreviations (1K, 2M, 30s, etc.)
7070+ - Capitalization patterns (UPPERCASE → AWOO, Capitalized → Awoo)
7171+ - Punctuation and formatting
7272+ - Navigation and interactive elements
7373+- **Scroll to Top**: Smooth scroll-to-top button for long pages
7474+- **Responsive Design**: Mobile-first layout that adapts to all screen sizes
7575+- **SEO Optimization**: Comprehensive meta tags, Open Graph, and Twitter Card support
7676+- **RSS/Atom Feeds**: Multiple feed endpoints for blog posts and status updates
73777474-1. Install dependencies:
7878+### Technical Features
75797676-```bash
7777-npm install
7878-```
8080+- **Type-Safe Development**: Full TypeScript support with comprehensive type definitions
8181+- **Smart Error Handling**: Graceful degradation with informative error states
8282+- **Loading States**: Skeleton loaders for all async content
8383+- **Image Optimization**: Lazy loading and responsive image handling
8484+- **Blob URL Construction**: Proper PDS blob URL generation for media assets
8585+- **Media Extraction**: Automatic CID extraction from various image object formats
8686+- **Facet Processing**: Rich text with link detection and mention highlighting
79878080-1. Configure environment variables:
8888+## 📋 Configuration
81898282-Copy the example environment file and update it with your own information:
9090+Before using this template, you'll need to update several configuration files with your own information:
83918484-```bash
8585-cp .env .env.local
8686-```
9292+### Environment Variables (`.env`)
87938888-Edit `.env.local` with your settings:
9494+Create a `.env.local` file with your configuration:
89959096```ini
9197# Required: Your AT Protocol DID
···94100# Optional: Enable WhiteWind blog support (default: false)
95101PUBLIC_ENABLE_WHITEWIND=false
96102103103+# Optional: Custom domain for Leaflet publications
104104+PUBLIC_LEAFLET_BASE_PATH=https://blog.example.com
105105+97106# Optional: Fallback URL for missing blog posts
9898-PUBLIC_BLOG_FALLBACK_URL=
107107+PUBLIC_BLOG_FALLBACK_URL=https://archive.example.com/blog
99108100109# Site metadata
101110PUBLIC_SITE_TITLE="Your Site Title"
102111PUBLIC_SITE_DESCRIPTION="Your site description"
103103-PUBLIC_SITE_KEYWORDS="keywords, here"
112112+PUBLIC_SITE_KEYWORDS="keywords, separated, by, commas"
104113PUBLIC_SITE_URL="https://example.com"
105114```
106115107107-1. Configure your publication slugs in `src/lib/config/slugs.ts`:
116116+### Publication Slug Mappings (`src/lib/config/slugs.ts`)
117117+118118+Map friendly URLs to your Leaflet publications:
108119109120```typescript
110121export const slugMappings: SlugMapping[] = [
111111- {
112112- slug: 'blog',
113113- publicationRkey: '3m3x4bgbsh22k' // Your publication rkey
114114- }
115115- // Add more mappings as needed:
116116- // { slug: 'notes', publicationRkey: 'xyz123abc' },
117117- // { slug: 'essays', publicationRkey: 'def456ghi' },
122122+ { slug: 'blog', publicationRkey: '3m3x4bgbsh22k' },
123123+ { slug: 'essays', publicationRkey: 'abc123xyz' },
124124+ { slug: 'notes', publicationRkey: 'def456uvw' }
118125];
119126```
120127121121-1. Start the development server:
128128+### Static Files
129129+130130+Update or remove these files that are specific to the example site:
131131+132132+- `static/robots.txt` - Update the sitemap URL
133133+- `static/sitemap.xml` - Update with your domain and pages
134134+- `static/.well-known/*` - Replace with your own well-known files
135135+- `static/favicon/` - Replace with your branding
136136+137137+## 🚀 Getting Started
138138+139139+### Prerequisites
140140+141141+- Node.js 18+ and npm
142142+- An AT Protocol DID (Decentralized Identifier) from Bluesky
143143+144144+### Installation
145145+146146+1. **Clone the repository**:
147147+148148+ ```bash
149149+ git clone git@github.com:ewanc26/website.git
150150+ cd website
151151+ ```
152152+153153+2. **Install dependencies**:
154154+155155+ ```bash
156156+ npm install
157157+ ```
158158+159159+3. **Configure environment variables**:
122160123123-```bash
124124-npm run dev
125125-```
161161+ ```bash
162162+ cp .env .env.local
163163+ ```
164164+165165+ Edit `.env.local` with your settings (see Configuration section above)
166166+167167+4. **Configure publication slugs** in `src/lib/config/slugs.ts`
168168+169169+5. **Start the development server**:
170170+171171+ ```bash
172172+ npm run dev
173173+ ```
126174127127-Visit `http://localhost:5173` to view your site.
175175+ Visit `http://localhost:5173` to view your site
128176129177## 📁 Project Structure
130178131179```text
132132-website-redesign/
180180+website/
133181├── src/
134182│ ├── lib/
135135-│ │ ├── assets/ # Static assets (images, icons)
136136-│ │ ├── components/ # Reusable Svelte components
137137-│ │ │ ├── layout/ # Header, Footer, Navigation
138138-│ │ │ └── ui/ # UI components (Card, etc.)
139139-│ │ ├── config/ # Configuration files
140140-│ │ │ └── slugs.ts # Slug to publication mapping
141141-│ │ ├── data/ # Static data (navigation items)
142142-│ │ ├── helper/ # Helper functions (meta tags, OG images)
143143-│ │ ├── services/ # External service integrations
144144-│ │ │ └── atproto/ # AT Protocol service layer
145145-│ │ └── utils/ # Utility functions
146146-│ ├── routes/ # SvelteKit routes
147147-│ │ ├── [slug]/ # Dynamic slug-based publication routes
148148-│ │ ├── now/ # Status-feed endpoints
149149-│ │ └── site/ # Site-metadata pages
150150-│ ├── app.css # Global styles
151151-│ └── app.html # HTML template
152152-├── static/ # Static files (favicon, etc.)
183183+│ │ ├── assets/ # Static assets (images, icons)
184184+│ │ ├── components/ # Reusable Svelte components
185185+│ │ │ ├── layout/ # Header, Footer, Navigation, ThemeToggle, WolfToggle
186186+│ │ │ │ └── main/
187187+│ │ │ │ ├── card/ # ProfileCard, MusicStatusCard, etc.
188188+│ │ │ │ ├── DynamicLinks.svelte
189189+│ │ │ │ ├── ScrollToTop.svelte
190190+│ │ │ │ └── TangledRepos.svelte
191191+│ │ │ ├── seo/ # MetaTags component
192192+│ │ │ └── ui/ # Reusable UI components (Card, etc.)
193193+│ │ ├── config/ # Configuration files
194194+│ │ │ └── slugs.ts # Slug to publication mapping
195195+│ │ ├── data/ # Static data (navigation items)
196196+│ │ ├── helper/ # Helper functions (meta tags, OG images)
197197+│ │ ├── services/ # External service integrations
198198+│ │ │ └── atproto/ # AT Protocol service layer
199199+│ │ │ ├── agents.ts # Agent management & PDS resolution
200200+│ │ │ ├── cache.ts # In-memory caching
201201+│ │ │ ├── engagement.ts # Post engagement (likes/reposts)
202202+│ │ │ ├── fetch.ts # Profile, status, site info, music status
203203+│ │ │ ├── media.ts # Blob URL & image handling
204204+│ │ │ ├── musicbrainz.ts # MusicBrainz API integration
205205+│ │ │ ├── posts.ts # Blog posts, Bluesky posts, publications
206206+│ │ │ ├── tangled.ts # Tangled repository fetching
207207+│ │ │ └── types.ts # TypeScript type definitions
208208+│ │ ├── stores/ # Svelte stores
209209+│ │ │ └── wolfMode.ts # Wolf mode text transformation
210210+│ │ └── utils/ # Utility functions (date formatting, etc.)
211211+│ ├── routes/ # SvelteKit routes
212212+│ │ ├── [slug=slug]/ # Dynamic slug-based publication routes
213213+│ │ │ ├── [rkey]/ # Individual document redirects
214214+│ │ │ ├── atom/ # Deprecated Atom feeds (410 Gone)
215215+│ │ │ └── rss/ # RSS feed endpoints
216216+│ │ ├── favicon.ico/ # Favicon endpoint
217217+│ │ ├── now/ # Status feed endpoints
218218+│ │ │ ├── atom/ # Deprecated Atom feeds
219219+│ │ │ └── rss/ # RSS feeds
220220+│ │ └── site/
221221+│ │ └── meta/ # Site metadata page
222222+│ ├── app.css # Global styles
223223+│ └── app.html # HTML template
224224+├── static/ # Static files (favicon, robots.txt, etc.)
153225└── package.json
154226```
155227···160232### Core Services
161233162234- **agents.ts**: Agent management with automatic PDS resolution and fallback to the Bluesky public API
163163-- **fetch.ts**: Profile, status, site info, and links fetching
164164-- **posts.ts**: Blog posts, Leaflet publications, and Bluesky posts
165165-- **media.ts**: Image and blob-URL handling
166166-- **cache.ts**: In-memory caching with TTL support
167167-- **types.ts**: TypeScript definitions for all data structures
235235+- **fetch.ts**: Profile, status, site info, links, and music status fetching
236236+- **posts.ts**: Blog posts (WhiteWind & Leaflet), Bluesky posts, and publications
237237+- **tangled.ts**: Repository information from Tangled lexicon
238238+- **engagement.ts**: Post engagement data (likes/reposts) via Constellation API
239239+- **media.ts**: Image and blob URL handling with CID extraction
240240+- **musicbrainz.ts**: MusicBrainz API integration for album artwork
241241+- **cache.ts**: In-memory caching with configurable TTL support
242242+- **types.ts**: Comprehensive TypeScript definitions for all data structures
168243169169-### Usage Example
244244+### Usage Examples
170245171246```typescript
172172-import { fetchProfile, fetchBlogPosts, fetchLatestBlueskyPost } from '$lib/services/atproto';
247247+import {
248248+ fetchProfile,
249249+ fetchBlogPosts,
250250+ fetchLatestBlueskyPost,
251251+ fetchMusicStatus,
252252+ fetchTangledRepos
253253+} from '$lib/services/atproto';
173254174255// Fetch profile data
175256const profile = await fetchProfile();
176257177177-// Fetch blog posts from WhiteWind and Leaflet
258258+// Fetch blog posts from WhiteWind and/or Leaflet
178259const { posts } = await fetchBlogPosts();
179260180261// Fetch latest Bluesky post
181262const post = await fetchLatestBlueskyPost();
263263+264264+// Fetch current or last played music
265265+const musicStatus = await fetchMusicStatus();
266266+267267+// Fetch code repositories
268268+const repos = await fetchTangledRepos();
182269```
183270184271## 📝 Publication System
···192279```typescript
193280export const slugMappings: SlugMapping[] = [
194281 {
195195- slug: 'blog', // Access via /blog
282282+ slug: 'blog', // Access via /blog
196283 publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey
197284 },
198285 {
199199- slug: 'notes', // Access via /notes
286286+ slug: 'notes', // Access via /notes
200287 publicationRkey: 'xyz123abc'
201288 }
202289];
···204291205292### Supported Platforms
206293207207-1. **Leaflet*- (`pub.leaflet.document`) – **Prioritised by default**
208208-294294+1. **Leaflet** (`pub.leaflet.document`) – **Prioritized by default**
209295 - Format: Custom domain or `https://leaflet.pub/lish/{did}/{publication}/{rkey}`
210296 - Supports multiple publications via slug mapping
211297 - Respects `base_path` configuration
212298 - Always checked first
213299214214-2. **WhiteWind*- (`com.whtwnd.blog.entry`) – **Optional, disabled by default**
215215-300300+2. **WhiteWind** (`com.whtwnd.blog.entry`) – **Optional, disabled by default**
216301 - Format: `https://whtwnd.com/{did}/{rkey}`
217302 - Automatically filters out drafts and non-public posts
218303 - Only checked if `PUBLIC_ENABLE_WHITEWIND=true`
···222307- `/{slug}` – Redirects to your publication homepage (configured in slugs.ts)
223308- `/{slug}/{rkey}` – Smart redirect to the correct platform (checks Leaflet first, then WhiteWind if enabled)
224309- `/{slug}/rss` – Intelligent RSS feed (redirects to Leaflet RSS by default, or generates WhiteWind RSS if enabled)
225225-- `/{slug}/atom` – Deprecated (returns *410 Gone*, use RSS instead)
310310+- `/{slug}/atom` – Deprecated (returns 410 Gone, use RSS instead)
226311227227-### How It Works
312312+### Priority Order
228313229229-**Priority Order:**
230230-231231-1. **Leaflet*- is always checked first for publications and documents
314314+1. **Leaflet** is always checked first for publications and documents
2323152. The slug mapping determines which publication to check
233233-3. **WhiteWind*- is only checked if `PUBLIC_ENABLE_WHITEWIND=true`
316316+3. **WhiteWind** is only checked if `PUBLIC_ENABLE_WHITEWIND=true`
2343174. If neither platform has the document, it falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured
235235-5. Returns *404- if the document isn't found and no fallback is set
318318+5. Returns 404 if the document isn't found and no fallback is set
236319237237-**When a user visits `/{slug}/{rkey}`:**
238238-239239-1. The system looks up the publication rkey from the slug configuration
240240-2. It checks Leaflet for the document in that specific publication
241241-3. If not found and WhiteWind is enabled, it checks WhiteWind
242242-4. Redirects to the appropriate platform URL
243243-5. Falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured
244244-6. Returns 404 if no document is found and no fallback exists
245245-246246-**RSS Feed Behaviour:**
320320+### RSS Feed Behavior
247321248248-- **WhiteWind disabled*- (default): Redirects to Leaflet's native RSS feed (includes full content)
322322+- **WhiteWind disabled** (default): Redirects to Leaflet's native RSS feed (includes full content)
249323- **WhiteWind enabled with posts**: Generates an RSS feed with WhiteWind post links
250324- **No posts found**: Returns 404
251325252252-### Platform Configuration (Leaflet / WhiteWind)
326326+### Finding Your Publication Rkey
253327254254-Control publication behaviour with environment variables:
328328+1. Visit your Leaflet publication page
329329+2. The URL will be in the format: `https://leaflet.pub/lish/{did}/{rkey}`
330330+3. Copy the `{rkey}` part (e.g., `3m3x4bgbsh22k`)
331331+4. Add it to your slug mapping in `src/lib/config/slugs.ts`
255332256256-```ini
257257-# Use a custom domain for Leaflet posts (recommended)
258258-PUBLIC_LEAFLET_BASE_PATH=https://blog.example.com
333333+## 🎵 Music Integration
259334260260-# Enable WhiteWind support (set to "true" to enable, default: "false")
261261-PUBLIC_ENABLE_WHITEWIND=false
335335+The site displays your music listening activity via teal.fm integration:
262336263263-# Fallback for missing posts
264264-PUBLIC_BLOG_FALLBACK_URL=https://archive.example.com/blog
265265-```
337337+### Supported Record Types
266338267267-And configure your slug mappings in `src/lib/config/slugs.ts`:
339339+- **`fm.teal.alpha.actor.status`**: Current "Now Playing" status with expiry
340340+- **`fm.teal.alpha.feed.play`**: Historical play records
268341269269-```typescript
270270-export const slugMappings: SlugMapping[] = [
271271- { slug: 'blog', publicationRkey: '3m3x4bgbsh22k' },
272272- { slug: 'essays', publicationRkey: 'abc123xyz' },
273273- { slug: 'notes', publicationRkey: 'def456uvw' }
274274-];
275275-```
342342+### Album Artwork System
276343277277-### Why Leaflet is Prioritised
344344+The music card uses a sophisticated artwork retrieval system:
278345279279-- **Better Performance**: Leaflet's RSS feeds include full post content
280280-- **Custom Domains**: Each publication can have its own `base_path` configured in Leaflet
281281-- **Rich Features**: Better media handling and publication management
282282-- **Multiple Publications**: Easy management of multiple publications with slug mapping
283283-- **Active Development**: Leaflet is actively maintained and improved
346346+1. **MusicBrainz Cover Art Archive** (Primary)
347347+ - Uses `releaseMbId` from music records
348348+ - Free, no API key required
349349+ - Automatic search fallback when IDs are missing
350350+ - Caches search results to avoid repeated lookups
284351285285-### Enabling WhiteWind
352352+2. **AT Protocol Blob Storage** (Fallback)
353353+ - Uses `artwork` field from records
354354+ - Proper PDS blob URL construction
286355287287-If you still use WhiteWind or want to support both platforms:
356356+### Features
288357289289-```ini
290290-PUBLIC_ENABLE_WHITEWIND=true
291291-```
358358+- Displays track name, artists, album, and duration
359359+- Shows relative timestamps ("2 minutes ago")
360360+- Links to origin URLs (Last.fm, Spotify, etc.)
361361+- Responsive artwork display with fallback icons
362362+- Smart caching with 5-minute TTL
363363+- Automatic status expiry handling
292364293293-With WhiteWind enabled:
365365+### Configuration
294366295295-- Documents are checked on both platforms (Leaflet first, then WhiteWind)
296296-- RSS feed includes WhiteWind posts if they exist
297297-- `/{slug}` redirects to WhiteWind if no Leaflet configuration is set
367367+Set your DID in `.env.local` to fetch your music status:
298368299299-### Finding Your Publication Rkey
369369+```ini
370370+PUBLIC_ATPROTO_DID=did:plc:your-did-here
371371+```
300372301301-1. Visit your Leaflet publication page
302302-2. The URL will be in the format: `https://leaflet.pub/lish/{did}/{rkey}`
303303-3. Copy the `{rkey}` part (e.g., `3m3x4bgbsh22k`)
304304-4. Add it to your slug mapping in `src/lib/config/slugs.ts`
373373+The card will automatically display your current or last played track.
305374306375## 🎨 Styling
307376308377The project uses:
309378310310-- **Tailwind CSS 4**: Latest Tailwind with new features
311311-- **@tailwindcss/typography**: Beautiful prose styling
312312-- **@tailwindcss/vite**: Vite plugin for Tailwind
313313-- **Custom Components**: Pre-built UI components with consistent styling
379379+- **Tailwind CSS 4**: Latest Tailwind with new features and improved performance
380380+- **@tailwindcss/typography**: Beautiful prose styling for blog content
381381+- **@tailwindcss/vite**: Vite plugin for optimal Tailwind integration
382382+- **Custom Color Palette**: Semantic color tokens (canvas, ink, primary) for consistent theming
383383+- **Dark Mode**: System preference detection with manual override
384384+- **Responsive Design**: Mobile-first approach with breakpoint utilities
314385315386## 🏗️ Building for Production
316387···328399329400This project uses `@sveltejs/adapter-auto`, which automatically selects the best adapter for your deployment platform:
330401331331-- **Vercel**: Automatic detection and optimisation
332332-- **Netlify**: Automatic detection and optimisation
333333-- **Cloudflare Pages**: Automatic detection and optimisation
402402+- **Vercel**: Automatic detection and optimization
403403+- **Netlify**: Automatic detection and optimization
404404+- **Cloudflare Pages**: Automatic detection and optimization
334405- **Node.js**: Fallback option
335406336407For other platforms, see the [SvelteKit adapters documentation](https://kit.svelte.dev/docs/adapters).
···339410340411The site supports several custom AT Protocol lexicons:
341412342342-### Status Updates (`uk.ewancroft.now`)
343343-344344-Display real-time status messages on your site.
345345-346413### Site Information (`uk.ewancroft.site.info`)
347414348348-Store detailed site metadata including:
415415+Store comprehensive site metadata:
349416350417- Technology stack
351418- Privacy statement
352419- Open-source information
353353-- Credits and licences
420420+- Credits and licenses
421421+- Related services
354422355423### Link Board (`blue.linkat.board`)
356424357425Display a collection of links with emoji icons.
358426427427+### Music Status (`fm.teal.alpha.actor.status` & `fm.teal.alpha.feed.play`)
428428+429429+Show music listening activity via teal.fm integration.
430430+431431+### Tangled Repositories (`sh.tangled.repo`)
432432+433433+Display code repositories with descriptions, labels, and metadata.
434434+359435## 🛠️ Development
360436361437### Available Scripts
···372448373449The project uses:
374450375375-- **TypeScript*- – Full type safety
376376-- **Prettier*- – Consistent code formatting
377377-- **svelte-check*- – Svelte-specific linting
378378-- **ESLint*- – (can be added if needed)
379379-380380-## 📚 Reference Implementation
381381-382382-The `other/leaflet-main` directory contains the reference Leaflet implementation, which was used as inspiration for:
383383-384384-- Feed generation
385385-- Publication handling
386386-- Document rendering
387387-- Bluesky integration
451451+- **TypeScript** – Full type safety throughout
452452+- **Prettier** – Consistent code formatting
453453+- **svelte-check** – Svelte-specific linting
454454+- **Svelte 5 Runes** – Modern reactivity with better performance
388455389456## 🤝 Contributing
390457391458Contributions are welcome! Please feel free to submit a pull request.
392459393393-## 📄 Licence
460460+1. Fork the repository
461461+2. Create your feature branch (`git checkout -b feature/amazing-feature`)
462462+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
463463+4. Push to the branch (`git push origin feature/amazing-feature`)
464464+5. Open a Pull Request
394465395395-This project is open-source. See the [licence](./LICENCE) file for more details on the website source code specifically and the [third party licences](./THIRD-PARTY-LICENSES.txt) file for the rest.
466466+## 📄 License
467467+468468+This project is open-source. See the [LICENSE](./LICENSE) file for more details on the website source code specifically and the [THIRD-PARTY-LICENSES.txt](./THIRD-PARTY-LICENSES.txt) file for third-party dependencies.
396469397470## 🔗 Links
398471···402475- [Bluesky](https://bsky.app/)
403476- [WhiteWind](https://whtwnd.com/)
404477- [Leaflet](https://leaflet.pub/)
478478+- [teal.fm](https://teal.fm/)
479479+- [MusicBrainz](https://musicbrainz.org/)
480480+- [Tangled](https://tangled.sh/)
481481+- [Linkat](https://linkat.blue/)
405482406406-## 💡 Tips
483483+## 💡 Tips & Troubleshooting
407484408485### Finding Your DID
409486410410-1. visit [PDSls](https://pdsls.dev/)
411411-1. enter your handle (i.e. `ewancroft.uk`)
412412-1. you will see a `did:plc` (or `did:web`) in the Repository field, that is your DID
413413-414414-> if not, select the arrow to the right of the text, it might show your handle instead.
487487+1. Visit [PDSls](https://pdsls.dev/)
488488+2. Enter your handle (e.g., `ewancroft.uk`)
489489+3. Look for the `did:plc` (or `did:web`) in the Repository field
490490+4. If not visible, click the arrow to the right of the text
415491416492### Cache Management
417493···425501426502// Clear a specific entry
427503cache.delete('profile:did:plc:...');
504504+505505+// Get cache statistics
506506+const profile = cache.get<ProfileData>('profile:did:plc:...');
428507```
429508430430-### Custom Components
509509+### Music Status Not Showing Artwork
431510432432-All components are built with Svelte 5 runes for better reactivity and performance. See the components directory for examples.
511511+If your music status doesn't show album artwork:
433512434434-## 🐛 Troubleshooting
513513+1. Ensure your scrobbler (e.g., piper) is including `releaseMbId` in records
514514+2. The system will automatically search MusicBrainz if IDs are missing
515515+3. Check browser console for MusicBrainz search results
516516+4. Fallback to blob storage if available
517517+5. Icon placeholder displays if no artwork is found
435518436519### Documents Not Found
437520438438-1. Check your `PUBLIC_ATPROTO_DID` is correct
439439-2. Verify the slug mapping in `src/lib/config/slugs.ts` is correct
440440-3. Ensure the publication rkey matches your Leaflet publication
441441-4. Verify documents are not drafts (WhiteWind) or unpublished (Leaflet)
442442-5. If using WhiteWind, ensure `PUBLIC_ENABLE_WHITEWIND=true` is set
443443-6. Check the browser console for AT Protocol service errors
444444-445445-### Slug Not Found
446446-447447-1. Add your slug mapping to `src/lib/config/slugs.ts`
448448-2. Ensure the format is: `{ slug: 'your-slug', publicationRkey: 'your-rkey' }`
449449-3. Restart the development server after changes
521521+1. Verify `PUBLIC_ATPROTO_DID` is correct
522522+2. Check slug mapping in `src/lib/config/slugs.ts`
523523+3. Ensure publication rkey matches your Leaflet publication
524524+4. Verify documents are published (not drafts)
525525+5. If using WhiteWind, ensure `PUBLIC_ENABLE_WHITEWIND=true`
526526+6. Check browser console for AT Protocol service errors
450527451451-### Profile Data Not Loading
528528+### Wolf Mode Not Working
452529453453-1. Ensure your DID is publicly accessible
454454-2. Check the browser console for errors
455455-3. Verify AT Protocol services are reachable
530530+1. Ensure JavaScript is enabled
531531+2. Check browser console for errors
532532+3. Wolf mode preserves navigation and interactive elements
533533+4. Numbers and abbreviations are preserved intentionally
456534457535### Build Errors
458536459459-1. Clear the `.svelte-kit` directory: `rm -rf .svelte-kit`
537537+1. Clear `.svelte-kit` directory: `rm -rf .svelte-kit`
4605382. Remove `node_modules`: `rm -rf node_modules`
461461-3. Reinstall dependencies: `npm install`
462462-4. Try building again: `npm run build`
539539+3. Clear package lock: `rm package-lock.json`
540540+4. Reinstall: `npm install`
541541+5. Try building: `npm run build`
463542464543## 🙏 Acknowledgements
465544466466-- Thanks to the AT Protocol team for creating an open protocol
467467-- Thanks to the Bluesky, WhiteWind, and Leaflet teams
545545+- Thanks to the AT Protocol team for creating an open, decentralized protocol
546546+- Thanks to the Bluesky, WhiteWind, Leaflet, teal.fm, Tangled, and Linkat teams
547547+- Thanks to MusicBrainz for providing free album artwork via the Cover Art Archive
468548- Inspired by the personal-web movement and IndieWeb principles
549549+- Built with love using modern web technologies
469550470551---
471552472472-Built with ❤️ using SvelteKit and AT Protocol
553553+Built with ❤️ using SvelteKit, AT Protocol, and open-source tools