Ewan's Corner#
A modern, AT Protocol-powered personal website built with SvelteKit 2 and Tailwind CSS 4.
🌟 Features#
- AT Protocol Integration: Fetch and display content from your AT Protocol repository
- Multi-Publication Support: Map friendly URL slugs to Leaflet publications with a simple config
- Multi-Platform Blog: Seamlessly aggregate blog posts from WhiteWind and/or Leaflet (configurable)
- Dynamic Profile: Automatically display your Bluesky profile information
- Custom Status: Show real-time status updates using custom AT Protocol lexicons
- Link Board: Display a Linkat board with emoji-styled link cards
- Bluesky Posts: Showcase your latest non-reply Bluesky posts with rich media support
- Smart Redirects: Intelligent redirection system for publication URLs with platform prioritisation
- Responsive Design: Mobile-first layout with dark-mode support
- RSS Feed: Intelligent RSS-feed handling for WhiteWind and/or Leaflet posts
- Type-Safe: Full TypeScript support throughout the application
🚀 Getting Started#
Prerequisites#
- Node.js 18+ and npm
- An AT Protocol DID (Decentralised Identifier) from Bluesky
Installation#
- Clone the repository:
git clone git@github.com:ewanc26/website.git
cd website-redesign
- Install dependencies:
npm install
- Configure environment variables:
Copy the example environment file and update it with your own information:
cp .env .env.local
Edit .env.local with your settings:
# Required: Your AT Protocol DID
PUBLIC_ATPROTO_DID=did:plc:your-did-here
# Optional: Enable WhiteWind blog support (default: false)
PUBLIC_ENABLE_WHITEWIND=false
# Optional: Fallback URL for missing blog posts
PUBLIC_BLOG_FALLBACK_URL=
# Site metadata
PUBLIC_SITE_TITLE="Your Site Title"
PUBLIC_SITE_DESCRIPTION="Your site description"
PUBLIC_SITE_KEYWORDS="keywords, here"
PUBLIC_SITE_URL="https://example.com"
- Configure your publication slugs in
src/lib/config/slugs.ts:
export const slugMappings: SlugMapping[] = [
{
slug: 'blog',
publicationRkey: '3m3x4bgbsh22k' // Your publication rkey
}
// Add more mappings as needed:
// { slug: 'notes', publicationRkey: 'xyz123abc' },
// { slug: 'essays', publicationRkey: 'def456ghi' },
];
- Start the development server:
npm run dev
Visit http://localhost:5173 to view your site.
📁 Project Structure#
website-redesign/
├── src/
│ ├── lib/
│ │ ├── assets/ # Static assets (images, icons)
│ │ ├── components/ # Reusable Svelte components
│ │ │ ├── layout/ # Header, Footer, Navigation
│ │ │ └── ui/ # 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
│ │ └── utils/ # Utility functions
│ ├── routes/ # SvelteKit routes
│ │ ├── [slug]/ # Dynamic slug-based publication routes
│ │ ├── now/ # Status-feed endpoints
│ │ └── site/ # Site-metadata pages
│ ├── app.css # Global styles
│ └── app.html # HTML template
├── static/ # Static files (favicon, 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, and links fetching
- posts.ts: Blog posts, Leaflet publications, and Bluesky posts
- media.ts: Image and blob-URL handling
- cache.ts: In-memory caching with TTL support
- types.ts: TypeScript definitions for all data structures
Usage Example#
import { fetchProfile, fetchBlogPosts, fetchLatestBlueskyPost } from '$lib/services/atproto';
// Fetch profile data
const profile = await fetchProfile();
// Fetch blog posts from WhiteWind and Leaflet
const { posts } = await fetchBlogPosts();
// Fetch latest Bluesky post
const post = await fetchLatestBlueskyPost();
📝 Publication System#
The publication system uses friendly URL slugs that map to Leaflet publications, with support for multiple platforms and intelligent URL redirects.
Slug Configuration#
Publications are mapped to URL slugs in src/lib/config/slugs.ts:
export const slugMappings: SlugMapping[] = [
{
slug: 'blog', // Access via /blog
publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey
},
{
slug: 'notes', // Access via /notes
publicationRkey: 'xyz123abc'
}
];
Supported Platforms#
-
Leaflet (
pub.leaflet.document) – Prioritised 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:
Publication Routes#
/{slug}– Redirects to your publication homepage (configured in slugs.ts)/{slug}/{rkey}– Smart redirect to the correct platform (checks Leaflet first, then WhiteWind if enabled)/{slug}/rss– Intelligent RSS feed (redirects to Leaflet RSS by default, or generates WhiteWind RSS if enabled)/{slug}/atom– Deprecated (returns 410 Gone, use RSS instead)
How It Works#
Priority Order:
- 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
When a user visits /{slug}/{rkey}:
- The system looks up the publication rkey from the slug configuration
- It checks Leaflet for the document in that specific publication
- If not found and WhiteWind is enabled, it checks WhiteWind
- Redirects to the appropriate platform URL
- Falls back to
PUBLIC_BLOG_FALLBACK_URLif configured - Returns 404 if no document is found and no fallback exists
RSS Feed Behaviour:
- 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
Configuration#
Control publication behaviour with environment variables:
# Use a custom domain for Leaflet posts (recommended)
PUBLIC_LEAFLET_BASE_PATH=https://blog.example.com
# Enable WhiteWind support (set to "true" to enable, default: "false")
PUBLIC_ENABLE_WHITEWIND=false
# Fallback for missing posts
PUBLIC_BLOG_FALLBACK_URL=https://archive.example.com/blog
And configure your slug mappings in src/lib/config/slugs.ts:
export const slugMappings: SlugMapping[] = [
{ slug: 'blog', publicationRkey: '3m3x4bgbsh22k' },
{ slug: 'essays', publicationRkey: 'abc123xyz' },
{ slug: 'notes', publicationRkey: 'def456uvw' }
];
Why Leaflet is Prioritised#
- Better Performance: Leaflet's RSS feeds include full post content
- Custom Domains: Each publication can have its own
base_pathconfigured in Leaflet - Rich Features: Better media handling and publication management
- Multiple Publications: Easy management of multiple publications with slug mapping
- Active Development: Leaflet is actively maintained and improved
Enabling WhiteWind#
If you still use WhiteWind or want to support both platforms:
PUBLIC_ENABLE_WHITEWIND=true
With WhiteWind enabled:
- Documents are checked on both platforms (Leaflet first, then WhiteWind)
- RSS feed includes WhiteWind posts if they exist
/{slug}redirects to WhiteWind if no Leaflet configuration is set
Finding Your Publication Rkey#
- 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
🎨 Styling#
The project uses:
- Tailwind CSS 4: Latest Tailwind with new features
- @tailwindcss/typography: Beautiful prose styling
- @tailwindcss/vite: Vite plugin for Tailwind
- Custom Components: Pre-built UI components with consistent styling
🏗️ Building for Production#
# Build the application
npm run build
# Preview the production build
npm run preview
The build output will be in the .svelte-kit directory, ready for deployment.
📦 Deployment#
This project uses @sveltejs/adapter-auto, which automatically selects the best adapter for your deployment platform:
- Vercel: Automatic detection and optimisation
- Netlify: Automatic detection and optimisation
- Cloudflare Pages: Automatic detection and optimisation
- Node.js: Fallback option
For other platforms, see the SvelteKit adapters documentation.
🔍 Custom Lexicons#
The site supports several custom AT Protocol lexicons:
Status Updates (uk.ewancroft.now)#
Display real-time status messages on your site.
Site Information (uk.ewancroft.site.info)#
Store detailed site metadata including:
- Technology stack
- Privacy statement
- Open-source information
- Credits and licences
Link Board (blue.linkat.board)#
Display a collection of links with emoji icons.
🛠️ 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
- Prettier – Consistent code formatting
- svelte-check – Svelte-specific linting
- ESLint – (can be added if needed)
📚 Reference Implementation#
The other/leaflet-main directory contains the reference Leaflet implementation, which was used as inspiration for:
- Feed generation
- Publication handling
- Document rendering
- Bluesky integration
🤝 Contributing#
Contributions are welcome! Please feel free to submit a pull request.
📄 Licence#
This project is open-source. See the Site Information section for details about the technology stack and licences.
🔗 Links#
- AT Protocol Documentation
- SvelteKit Documentation
- Tailwind CSS Documentation
- Bluesky
- WhiteWind
- Leaflet
💡 Tips#
Finding Your DID#
- Visit your Bluesky profile
- Click on Settings → Advanced
- Your DID will be displayed there
- Or visit
https://bsky.app/profile/{your-handle}and check the page source
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:...');
Custom Components#
All components are built with Svelte 5 runes for better reactivity and performance. See the components directory for examples.
🐛 Troubleshooting#
Documents Not Found#
- Check your
PUBLIC_ATPROTO_DIDis correct - Verify the slug mapping in
src/lib/config/slugs.tsis correct - Ensure the publication rkey matches your Leaflet publication
- Verify documents are not drafts (WhiteWind) or unpublished (Leaflet)
- If using WhiteWind, ensure
PUBLIC_ENABLE_WHITEWIND=trueis set - Check the browser console for AT Protocol service errors
Slug Not Found#
- Add your slug mapping to
src/lib/config/slugs.ts - Ensure the format is:
{ slug: 'your-slug', publicationRkey: 'your-rkey' } - Restart the development server after changes
Profile Data Not Loading#
- Ensure your DID is publicly accessible
- Check the browser console for errors
- Verify AT Protocol services are reachable
Build Errors#
- Clear the
.svelte-kitdirectory:rm -rf .svelte-kit - Remove
node_modules:rm -rf node_modules - Reinstall dependencies:
npm install - Try building again:
npm run build
🙏 Acknowledgements#
- Thanks to the AT Protocol team for creating an open protocol
- Thanks to the Bluesky, WhiteWind, and Leaflet teams
- Inspired by the personal-web movement and IndieWeb principles
Built with ❤️ using SvelteKit and AT Protocol