diff --git a/.cspell.json b/.cspell.json index 9d4a16a..50a2591 100644 --- a/.cspell.json +++ b/.cspell.json @@ -11,6 +11,7 @@ "bandcamp", "Batarong", "Behaviour", + "bgbsh", "blogposts", "blueskypost", "bradlc", diff --git a/README.md b/README.md index a603096..8729721 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ -# Ewan’s Corner +# 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 Blog Redirects**: Intelligent redirection system for blog-post URLs with platform prioritisation +- **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 @@ -51,12 +52,6 @@ Edit `.env.local` with your settings: # Required: Your AT Protocol DID PUBLIC_ATPROTO_DID=did:plc:your-did-here -# Optional: Custom Leaflet blog domain -PUBLIC_LEAFLET_BASE_PATH=https://blog.example.com - -# Optional: Specific Leaflet publication rkey for blog posts -PUBLIC_LEAFLET_BLOG_PUBLICATION= - # Optional: Enable WhiteWind blog support (default: false) PUBLIC_ENABLE_WHITEWIND=false @@ -70,7 +65,21 @@ PUBLIC_SITE_KEYWORDS="keywords, here" PUBLIC_SITE_URL="https://example.com" ``` -4. Start the development server: +4. Configure your publication slugs in `src/lib/config/slugs.ts`: + +```typescript +export const slugMappings: SlugMapping[] = [ + { + slug: 'blog', + publicationRkey: '3m3x4bgbsh22k' // Your publication rkey + } + // Add more mappings as needed: + // { slug: 'notes', publicationRkey: 'xyz123abc' }, + // { slug: 'essays', publicationRkey: 'def456ghi' }, +]; +``` + +5. Start the development server: ```bash npm run dev @@ -88,13 +97,15 @@ website-redesign/ β”‚ β”‚ β”œβ”€β”€ 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 -β”‚ β”‚ β”œβ”€β”€ blog/ # Blog redirect handlers +β”‚ β”‚ β”œβ”€β”€ [slug]/ # Dynamic slug-based publication routes β”‚ β”‚ β”œβ”€β”€ now/ # Status-feed endpoints β”‚ β”‚ └── site/ # Site-metadata pages β”‚ β”œβ”€β”€ app.css # Global styles @@ -131,15 +142,32 @@ const { posts } = await fetchBlogPosts(); const post = await fetchLatestBlueskyPost(); ``` -## πŸ“ Blog System +## πŸ“ 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 -The blog system supports multiple platforms with configurable prioritisation and intelligent URL redirects. +Publications are mapped to URL slugs in `src/lib/config/slugs.ts`: + +```typescript +export const slugMappings: SlugMapping[] = [ + { + slug: 'blog', // Access via /blog + publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey + }, + { + slug: 'notes', // Access via /notes + publicationRkey: 'xyz123abc' + } +]; +``` ### Supported Platforms 1. **Leaflet** (`pub.leaflet.document`) – **Prioritised by default** - Format: Custom domain or `https://leaflet.pub/lish/{did}/{publication}/{rkey}` - - Supports multiple publications + - Supports multiple publications via slug mapping - Respects `base_path` configuration - Always checked first @@ -148,47 +176,46 @@ The blog system supports multiple platforms with configurable prioritisation and - Automatically filters out drafts and non-public posts - Only checked if `PUBLIC_ENABLE_WHITEWIND=true` -### Blog Routes +### Publication Routes -- `/blog` – Redirects to your blog homepage (Leaflet by default, WhiteWind if configured) -- `/blog/{rkey}` – Smart redirect to the correct platform (checks Leaflet first, then WhiteWind if enabled) -- `/blog/rss` – Intelligent RSS feed (redirects to Leaflet RSS by default, or generates WhiteWind RSS if enabled) -- `/blog/atom` – Deprecated (returns *410 Gone*, use RSS instead) +- `/{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:** -1. **Leaflet** is always checked first for blog posts -2. **WhiteWind** is only checked if `PUBLIC_ENABLE_WHITEWIND=true` -3. If neither platform has the post, it falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured -4. Returns *404* if the post isn’t found and no fallback is set +1. **Leaflet** is always checked first for publications and documents +2. The slug mapping determines which publication to check +3. **WhiteWind** is only checked if `PUBLIC_ENABLE_WHITEWIND=true` +4. If neither platform has the document, it falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured +5. Returns *404* if the document isn't found and no fallback is set -**When a user visits `/blog/{rkey}`:** +**When a user visits `/{slug}/{rkey}`:** -1. The system checks Leaflet for the post (with optional publication filtering) -2. If not found and WhiteWind is enabled, it checks WhiteWind -3. Redirects to the appropriate platform URL -4. Falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured -5. Returns 404 if no post is found and no fallback exists +1. The system looks up the publication rkey from the slug configuration +2. It checks Leaflet for the document in that specific publication +3. If not found and WhiteWind is enabled, it checks WhiteWind +4. Redirects to the appropriate platform URL +5. Falls back to `PUBLIC_BLOG_FALLBACK_URL` if configured +6. 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 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 blog behaviour with environment variables: +Control publication behaviour with environment variables: ```ini # Use a custom domain for Leaflet posts (recommended) PUBLIC_LEAFLET_BASE_PATH=https://blog.example.com -# Only check a specific Leaflet publication -PUBLIC_LEAFLET_BLOG_PUBLICATION=3kzcijpj2z2a - # Enable WhiteWind support (set to "true" to enable, default: "false") PUBLIC_ENABLE_WHITEWIND=false @@ -196,11 +223,22 @@ PUBLIC_ENABLE_WHITEWIND=false PUBLIC_BLOG_FALLBACK_URL=https://archive.example.com/blog ``` +And configure your slug mappings in `src/lib/config/slugs.ts`: + +```typescript +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**: Native support for custom domains (e.g. `blog.example.com`) +- **Better Performance**: Leaflet's RSS feeds include full post content +- **Custom Domains**: Each publication can have its own `base_path` configured 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 @@ -213,9 +251,16 @@ PUBLIC_ENABLE_WHITEWIND=true With WhiteWind enabled: -- Blog posts are checked on both platforms (Leaflet first, then WhiteWind) +- Documents are checked on both platforms (Leaflet first, then WhiteWind) - RSS feed includes WhiteWind posts if they exist -- `/blog` redirects to WhiteWind if no Leaflet configuration is set +- `/{slug}` redirects to WhiteWind if no Leaflet configuration is set + +### Finding Your Publication Rkey + +1. Visit your Leaflet publication page +2. The URL will be in the format: `https://leaflet.pub/lish/{did}/{rkey}` +3. Copy the `{rkey}` part (e.g., `3m3x4bgbsh22k`) +4. Add it to your slug mapping in `src/lib/config/slugs.ts` ## 🎨 Styling @@ -346,13 +391,20 @@ All components are built with Svelte 5 runes for better reactivity and performan ## πŸ› Troubleshooting -### Blog Posts Not Found +### Documents Not Found 1. Check your `PUBLIC_ATPROTO_DID` is correct -2. Verify posts are not drafts (WhiteWind) or unpublished (Leaflet) -3. Check the publication configuration if using `PUBLIC_LEAFLET_BLOG_PUBLICATION` -4. If using WhiteWind, ensure `PUBLIC_ENABLE_WHITEWIND=true` is set -5. Check the browser console for AT Protocol service errors +2. Verify the slug mapping in `src/lib/config/slugs.ts` is correct +3. Ensure the publication rkey matches your Leaflet publication +4. Verify documents are not drafts (WhiteWind) or unpublished (Leaflet) +5. If using WhiteWind, ensure `PUBLIC_ENABLE_WHITEWIND=true` is set +6. Check the browser console for AT Protocol service errors + +### Slug Not Found + +1. Add your slug mapping to `src/lib/config/slugs.ts` +2. Ensure the format is: `{ slug: 'your-slug', publicationRkey: 'your-rkey' }` +3. Restart the development server after changes ### Profile Data Not Loading diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts new file mode 100644 index 0000000..8d1ab73 --- /dev/null +++ b/src/lib/config/index.ts @@ -0,0 +1 @@ +export * from './slugs'; diff --git a/src/lib/config/slugs.ts b/src/lib/config/slugs.ts new file mode 100644 index 0000000..d0d2e69 --- /dev/null +++ b/src/lib/config/slugs.ts @@ -0,0 +1,58 @@ +/** + * Slug to Leaflet Publication mapping configuration + * + * Maps friendly URL slugs to Leaflet publication rkeys. + * This allows you to access publications via /{slug} instead of /blog + * + * Example: + * - /blog β†’ maps to publication with rkey "3m3x4bgbsh22k" + * - /notes β†’ maps to publication with rkey "xyz123abc" + */ + +export interface SlugMapping { + /** The URL-friendly slug */ + slug: string; + /** The Leaflet publication rkey */ + publicationRkey: string; +} + +/** + * Slug to publication rkey mappings + * Add your custom mappings here + */ +export const slugMappings: SlugMapping[] = [ + { + slug: 'blog', + publicationRkey: '3m3x4bgbsh22k' // my blog publication rkey + }, + { + slug: 'cailean', + publicationRkey: '3m4222fxc3k2q' // Cailean Uen's publication rkey for his journal + } + // Add more mappings as needed: + // { slug: 'notes', publicationRkey: 'xyz123abc' }, + // { slug: 'essays', publicationRkey: 'def456ghi' }, +]; + +/** + * Get publication rkey from slug + */ +export function getPublicationRkeyFromSlug(slug: string): string | null { + const mapping = slugMappings.find(m => m.slug === slug); + return mapping?.publicationRkey || null; +} + +/** + * Get slug from publication rkey + */ +export function getSlugFromPublicationRkey(rkey: string): string | null { + const mapping = slugMappings.find(m => m.publicationRkey === rkey); + return mapping?.slug || null; +} + +/** + * Get all configured slugs + */ +export function getAllSlugs(): string[] { + return slugMappings.map(m => m.slug); +} diff --git a/src/lib/services/atproto/posts.ts b/src/lib/services/atproto/posts.ts index b8f6cc5..d3dde0e 100644 --- a/src/lib/services/atproto/posts.ts +++ b/src/lib/services/atproto/posts.ts @@ -1,4 +1,4 @@ -import { PUBLIC_ATPROTO_DID, PUBLIC_LEAFLET_BASE_PATH } from '$env/static/public'; +import { PUBLIC_ATPROTO_DID } from '$env/static/public'; import { cache } from './cache'; import { withFallback, defaultAgent } from './agents'; import { resolveIdentity } from './agents'; @@ -146,28 +146,26 @@ export async function fetchBlogPosts(): Promise { ); for (const record of leafletDocsRecords) { - const value = record.value as any; - const rkey = record.uri.split('/').pop() || ''; - const publicationUri = value.publication; - const publication = publicationsMap.get(publicationUri); - - // Determine URL based on priority: env var β†’ publication base_path β†’ Leaflet /lish format - let url: string; - const publicationRkey = publicationUri ? publicationUri.split('/').pop() : ''; - - if (PUBLIC_LEAFLET_BASE_PATH) { - url = `${PUBLIC_LEAFLET_BASE_PATH}/${rkey}`; - } else if (publication?.basePath) { - // Ensure basePath is a complete URL - const basePath = publication.basePath.startsWith('http') - ? publication.basePath - : `https://${publication.basePath}`; - url = `${basePath}/${rkey}`; - } else if (publicationRkey) { - url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}/${rkey}`; - } else { - url = `https://leaflet.pub/${PUBLIC_ATPROTO_DID}/${rkey}`; - } + const value = record.value as any; + const rkey = record.uri.split('/').pop() || ''; + const publicationUri = value.publication; + const publication = publicationsMap.get(publicationUri); + + // Determine URL based on priority: publication base_path β†’ Leaflet /lish format + let url: string; + const publicationRkey = publicationUri ? publicationUri.split('/').pop() : ''; + + if (publication?.basePath) { + // Ensure basePath is a complete URL + const basePath = publication.basePath.startsWith('http') + ? publication.basePath + : `https://${publication.basePath}`; + url = `${basePath}/${rkey}`; + } else if (publicationRkey) { + url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}/${rkey}`; + } else { + url = `https://leaflet.pub/${PUBLIC_ATPROTO_DID}/${rkey}`; + } posts.push({ title: value.title || 'Untitled Document', diff --git a/src/routes/[slug]/+server.ts b/src/routes/[slug]/+server.ts new file mode 100644 index 0000000..773f6e7 --- /dev/null +++ b/src/routes/[slug]/+server.ts @@ -0,0 +1,95 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import { PUBLIC_ATPROTO_DID } from '$env/static/public'; +import { fetchLeafletPublications } from '$lib/services/atproto'; +import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; + +/** + * Dynamic slug root redirect handler + * + * Redirects /{slug} to the appropriate Leaflet publication: + * - Uses the slug mapping config to find the publication rkey + * - Priority 1: Publication base_path from Leaflet API + * - Priority 2: Leaflet /lish format + * + * Individual posts are handled by the [rkey] route. + */ +export const GET: RequestHandler = async ({ params, url }) => { + const slug = params.slug; + + // If there's a path after /{slug}, let it fall through to other routes + const slugPath = url.pathname.replace(new RegExp(`^/${slug}/?`), ''); + + if (slugPath && !['rss', 'atom'].includes(slugPath)) { + // This will be caught by the [rkey] route + return new Response(null, { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }); + } + + // For /{slug} root, redirect to the publication + if (!slugPath || slugPath === '') { + // Get the publication rkey from the slug + const publicationRkey = getPublicationRkeyFromSlug(slug); + + if (!publicationRkey) { + return new Response( + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, + { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + } + ); + } + + let redirectUrl: string | null = null; + + try { + // Fetch publications to get base path + const { publications } = await fetchLeafletPublications(); + const publication = publications.find(p => p.rkey === publicationRkey); + + if (publication?.basePath) { + // Ensure basePath is a complete URL + redirectUrl = publication.basePath.startsWith('http') + ? publication.basePath + : `https://${publication.basePath}`; + } else { + // Use Leaflet /lish format + redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; + } + } catch (error) { + console.error('Error fetching Leaflet publication:', error); + // Fallback to /lish format + redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}`; + } + + // If we have a redirect URL, use it + if (redirectUrl) { + return new Response(null, { + status: 301, + headers: { + Location: redirectUrl, + 'Cache-Control': 'public, max-age=3600' + } + }); + } + + // No publication found + return new Response( + `Publication not found for slug: ${slug}\n\nPlease check your configuration in src/lib/config/slugs.ts`, + { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + } + ); + } + + return new Response(null, { status: 404 }); +}; diff --git a/src/routes/blog/[rkey]/+server.ts b/src/routes/[slug]/[rkey]/+server.ts similarity index 70% rename from src/routes/blog/[rkey]/+server.ts rename to src/routes/[slug]/[rkey]/+server.ts index 5faa490..82dca91 100644 --- a/src/routes/blog/[rkey]/+server.ts +++ b/src/routes/[slug]/[rkey]/+server.ts @@ -1,33 +1,31 @@ import type { RequestHandler } from '@sveltejs/kit'; import { PUBLIC_ATPROTO_DID, - PUBLIC_LEAFLET_BASE_PATH, - PUBLIC_LEAFLET_BLOG_PUBLICATION, PUBLIC_BLOG_FALLBACK_URL, PUBLIC_ENABLE_WHITEWIND } from '$env/static/public'; import { withFallback } from '$lib/services/atproto'; import { fetchLeafletPublications } from '$lib/services/atproto'; +import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; /** - * Smart blog post redirect handler + * Smart document redirect handler for slugged publications * * Automatically detects whether the post is from Leaflet or WhiteWind (if enabled) * and redirects to the appropriate URL. * * Priority order: - * 1. Leaflet: {LEAFLET_BASE_PATH}/{rkey} or https://leaflet.pub/{DID}/{rkey} + * 1. Leaflet: Uses publication's base_path or https://leaflet.pub/{DID}/{publicationRkey}/{rkey} * 2. WhiteWind: https://whtwnd.com/{DID}/{rkey} (only if PUBLIC_ENABLE_WHITEWIND is true) * * If detection fails, falls back to PUBLIC_BLOG_FALLBACK_URL or returns 404. * - * Supports multiple Leaflet publications: - * - If PUBLIC_LEAFLET_BLOG_PUBLICATION is set, only checks that specific publication - * - Otherwise, checks all publications for the document + * Uses slug mapping to determine which publication to check. */ async function detectPostPlatform( - rkey: string + rkey: string, + publicationRkey: string ): Promise<{ platform: 'whitewind' | 'leaflet' | 'unknown'; url?: string }> { try { // Check Leaflet FIRST (prioritized) using atproto services @@ -51,36 +49,32 @@ async function detectPostPlatform( if (leafletRecord) { const value = leafletRecord.value as any; - const publicationUri = value?.publication; + const documentPublicationUri = value?.publication; // Fetch publications to get base path const { publications } = await fetchLeafletPublications(); - const publication = publicationUri - ? publications.find((p) => p.uri === publicationUri) + const publication = documentPublicationUri + ? publications.find((p) => p.uri === documentPublicationUri) : null; - // If a specific blog publication is configured, check if this document belongs to it - if (PUBLIC_LEAFLET_BLOG_PUBLICATION && publication) { - if (publication.rkey !== PUBLIC_LEAFLET_BLOG_PUBLICATION) { - // Document belongs to a different publication, not the blog - return { platform: 'unknown' }; - } + // Check if this document belongs to the requested publication (from slug) + if (publication && publication.rkey !== publicationRkey) { + // Document belongs to a different publication + return { platform: 'unknown' }; } - // Determine URL based on priority: env var β†’ publication base_path β†’ Leaflet /lish format + // Determine URL based on publication base_path or Leaflet /lish format let url: string; - const publicationRkey = publication?.rkey || ''; + const docPublicationRkey = publication?.rkey || publicationRkey; - if (PUBLIC_LEAFLET_BASE_PATH) { - url = `${PUBLIC_LEAFLET_BASE_PATH}/${rkey}`; - } else if (publication?.basePath) { + if (publication?.basePath) { // Ensure basePath is a complete URL const basePath = publication.basePath.startsWith('http') ? publication.basePath : `https://${publication.basePath}`; url = `${basePath}/${rkey}`; - } else if (publicationRkey) { - url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}/${rkey}`; + } else if (docPublicationRkey) { + url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${docPublicationRkey}/${rkey}`; } else { url = `https://leaflet.pub/${PUBLIC_ATPROTO_DID}/${rkey}`; } @@ -131,8 +125,24 @@ async function detectPostPlatform( } export const GET: RequestHandler = async ({ params, url }) => { + const slug = params.slug; const rkey = params.rkey; + // Get the publication rkey from the slug + const publicationRkey = getPublicationRkeyFromSlug(slug); + + if (!publicationRkey) { + return new Response( + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, + { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + } + ); + } + // Validate TID format (AT Protocol record key) const tidPattern = /^[a-zA-Z0-9]{12,16}$/; @@ -146,7 +156,7 @@ export const GET: RequestHandler = async ({ params, url }) => { } // Detect platform and get appropriate URL - const detection = await detectPostPlatform(rkey); + const detection = await detectPostPlatform(rkey, publicationRkey); let targetUrl: string | null = null; let statusCode = 301; @@ -159,17 +169,15 @@ export const GET: RequestHandler = async ({ params, url }) => { targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`; } else { // No fallback configured, return 404 - const blogPublicationNote = PUBLIC_LEAFLET_BLOG_PUBLICATION - ? `\n\nNote: Only checking Leaflet publication: ${PUBLIC_LEAFLET_BLOG_PUBLICATION}` - : ''; + const publicationNote = `\n\nNote: Only checking Leaflet publication with rkey: ${publicationRkey}`; const whiteWindNote = PUBLIC_ENABLE_WHITEWIND === 'true' ? '\n- WhiteWind: https://whtwnd.com' : ''; return new Response( - `Blog post not found: ${rkey} + `Document not found: ${rkey} -This post could not be found on Leaflet${PUBLIC_ENABLE_WHITEWIND === 'true' ? ' or WhiteWind' : ''} platform${PUBLIC_ENABLE_WHITEWIND === 'true' ? 's' : ''}.${blogPublicationNote} +This document could not be found in the Leaflet publication for slug "${slug}"${PUBLIC_ENABLE_WHITEWIND === 'true' ? ' or WhiteWind' : ''}.${publicationNote} Please check: - Leaflet: https://leaflet.pub${whiteWindNote}`, @@ -194,4 +202,4 @@ Please check: 'Cache-Control': 'public, max-age=31536000, immutable' } }); -}; \ No newline at end of file +}; diff --git a/src/routes/blog/atom/+server.ts b/src/routes/[slug]/atom/+server.ts similarity index 65% rename from src/routes/blog/atom/+server.ts rename to src/routes/[slug]/atom/+server.ts index 5afd39b..a6ab0c9 100644 --- a/src/routes/blog/atom/+server.ts +++ b/src/routes/[slug]/atom/+server.ts @@ -1,4 +1,5 @@ import type { RequestHandler } from '@sveltejs/kit'; +import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; /** * Deprecated Atom feed @@ -11,13 +12,30 @@ import type { RequestHandler } from '@sveltejs/kit'; * - WhiteWind posts are included in our RSS feed * - Maintaining both RSS and Atom adds unnecessary complexity */ -export const GET: RequestHandler = () => { +export const GET: RequestHandler = ({ params }) => { + const slug = params.slug; + + // Validate slug exists in config + const publicationRkey = getPublicationRkeyFromSlug(slug); + + if (!publicationRkey) { + return new Response( + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, + { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + } + ); + } + return new Response( `Atom Feed Deprecated This Atom feed is no longer available. Please use the RSS feed instead: -RSS Feed: ${new URL('/blog/rss', 'http://localhost').pathname} +RSS Feed: /${slug}/rss For Leaflet posts with full content, the RSS feed will automatically redirect you to Leaflet's native RSS feed which includes complete post content. diff --git a/src/routes/blog/rss/+server.ts b/src/routes/[slug]/rss/+server.ts similarity index 67% rename from src/routes/blog/rss/+server.ts rename to src/routes/[slug]/rss/+server.ts index 9aee7a6..bae350b 100644 --- a/src/routes/blog/rss/+server.ts +++ b/src/routes/[slug]/rss/+server.ts @@ -4,40 +4,61 @@ import { PUBLIC_SITE_TITLE, PUBLIC_SITE_DESCRIPTION, PUBLIC_SITE_URL, - PUBLIC_LEAFLET_BASE_PATH, - PUBLIC_LEAFLET_BLOG_PUBLICATION, PUBLIC_ENABLE_WHITEWIND } from '$env/static/public'; import { fetchBlogPosts, fetchLeafletPublications } from '$lib/services/atproto'; +import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; /** - * RSS 2.0 feed for blog posts + * RSS 2.0 feed for publications (accessed via /{slug}/rss) * * Strategy: * 1. If WhiteWind is disabled or no WhiteWind posts exist, redirect to Leaflet RSS feed * 2. If WhiteWind is enabled and WhiteWind posts exist, generate RSS with WhiteWind posts * 3. If mixed content and WhiteWind is enabled, prioritize WhiteWind and generate RSS for those */ -export const GET: RequestHandler = async () => { +export const GET: RequestHandler = async ({ params }) => { + const slug = params.slug; + + // Get the publication rkey from the slug + const publicationRkey = getPublicationRkeyFromSlug(slug); + + if (!publicationRkey) { + return new Response( + `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, + { + status: 404, + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + } + ); + } + try { const { posts } = await fetchBlogPosts(); + // Filter posts for this specific publication + const publicationPosts = posts.filter( + p => p.publicationRkey === publicationRkey || p.platform === 'WhiteWind' + ); + // Separate WhiteWind and Leaflet posts - const whiteWindPosts = posts.filter((p) => p.platform === 'WhiteWind'); - const leafletPosts = posts.filter((p) => p.platform === 'leaflet'); + const whiteWindPosts = publicationPosts.filter((p) => p.platform === 'WhiteWind'); + const leafletPosts = publicationPosts.filter((p) => p.platform === 'leaflet'); // If WhiteWind is enabled and we have WhiteWind posts, generate RSS for them if (PUBLIC_ENABLE_WHITEWIND === 'true' && whiteWindPosts.length > 0) { - return generateWhiteWindRSS(whiteWindPosts); + return generateWhiteWindRSS(whiteWindPosts, slug); } // If WhiteWind is disabled or only Leaflet posts exist, redirect to Leaflet RSS feed if (leafletPosts.length > 0) { - return await redirectToLeafletRSS(); + return await redirectToLeafletRSS(publicationRkey); } // No posts at all - return new Response('No blog posts found', { + return new Response(`No posts found for publication: ${slug}`, { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8' @@ -57,15 +78,15 @@ export const GET: RequestHandler = async () => { /** * Generate RSS feed for WhiteWind posts */ -function generateWhiteWindRSS(posts: Array): Response { +function generateWhiteWindRSS(posts: Array, slug: string): Response { const rss = ` - ${escapeXml(PUBLIC_SITE_TITLE)} - Blog + ${escapeXml(PUBLIC_SITE_TITLE)} - ${slug} ${escapeXml(PUBLIC_SITE_URL)} ${escapeXml(PUBLIC_SITE_DESCRIPTION)} en - + ${new Date().toUTCString()} SvelteKit with AT Protocol ${posts @@ -96,36 +117,23 @@ ${posts /** * Redirect to Leaflet's native RSS feed */ -async function redirectToLeafletRSS(): Promise { +async function redirectToLeafletRSS(publicationRkey: string): Promise { try { const { publications } = await fetchLeafletPublications(); - // If a specific publication is configured, use that - if (PUBLIC_LEAFLET_BLOG_PUBLICATION) { - const publication = publications.find((p) => p.rkey === PUBLIC_LEAFLET_BLOG_PUBLICATION); - if (publication) { - const rssUrl = getLeafletRSSUrl(publication); - return Response.redirect(rssUrl, 307); // Temporary redirect - } - } - - // If there's only one publication, redirect to it - if (publications.length === 1) { - const rssUrl = getLeafletRSSUrl(publications[0]); + // Find the specific publication + const publication = publications.find((p) => p.rkey === publicationRkey); + + if (publication) { + const rssUrl = getLeafletRSSUrl(publication); return Response.redirect(rssUrl, 307); // Temporary redirect } - // Multiple publications but no specific one configured + // Publication not found return new Response( - `Multiple Leaflet publications found. Please configure PUBLIC_LEAFLET_BLOG_PUBLICATION in your .env file to specify which publication's RSS feed to use. - -Available publications: -${publications.map((p) => `- ${p.name} (rkey: ${p.rkey})`).join('\n')} - -Or visit the Leaflet RSS feeds directly: -${publications.map((p) => `- ${getLeafletRSSUrl(p)}`).join('\n')}`, + `Leaflet publication not found for rkey: ${publicationRkey}`, { - status: 300, // Multiple Choices + status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8' } @@ -146,10 +154,6 @@ ${publications.map((p) => `- ${getLeafletRSSUrl(p)}`).join('\n')}`, * Get the RSS URL for a Leaflet publication */ function getLeafletRSSUrl(publication: { basePath?: string; rkey: string }): string { - if (PUBLIC_LEAFLET_BASE_PATH) { - return `${PUBLIC_LEAFLET_BASE_PATH}/rss`; - } - if (publication.basePath) { // Ensure basePath is a complete URL const basePath = publication.basePath.startsWith('http') @@ -169,4 +173,4 @@ function escapeXml(unsafe: string): string { .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); -} \ No newline at end of file +} diff --git a/src/routes/blog/+server.ts b/src/routes/blog/+server.ts deleted file mode 100644 index 7fcca67..0000000 --- a/src/routes/blog/+server.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit'; -import { - PUBLIC_ATPROTO_DID, - PUBLIC_LEAFLET_BASE_PATH, - PUBLIC_LEAFLET_BLOG_PUBLICATION, - PUBLIC_ENABLE_WHITEWIND -} from '$env/static/public'; -import { fetchLeafletPublications } from '$lib/services/atproto'; - -/** - * Blog root redirect handler - * - * Redirects /blog to the appropriate blog platform: - * - Priority 1: Leaflet blog (if PUBLIC_LEAFLET_BASE_PATH is configured) - * - Priority 2: Leaflet publication page (if PUBLIC_LEAFLET_BLOG_PUBLICATION is set) - * - Fallback: WhiteWind blog page (if PUBLIC_ENABLE_WHITEWIND is true) - * - * Individual posts are handled by the [rkey] route which detects - * whether the post is from Leaflet or WhiteWind (if enabled). - */ -export const GET: RequestHandler = async ({ url }) => { - // If there's a path after /blog, let it fall through to other routes - const blogPath = url.pathname.replace(/^\/blog\/?/, ''); - - if (blogPath && !['rss', 'atom'].includes(blogPath)) { - // This will be caught by the [rkey] route - return new Response(null, { - status: 404, - headers: { - 'Content-Type': 'text/plain; charset=utf-8' - } - }); - } - - // For /blog root, redirect to the blog platform - if (!blogPath || blogPath === '') { - let redirectUrl: string | null = null; - - // Priority 1: Use Leaflet base path if configured - if (PUBLIC_LEAFLET_BASE_PATH) { - redirectUrl = PUBLIC_LEAFLET_BASE_PATH; - } - // Priority 2: Use Leaflet publication page if configured - else if (PUBLIC_LEAFLET_BLOG_PUBLICATION) { - try { - const { publications } = await fetchLeafletPublications(); - const publication = publications.find(p => p.rkey === PUBLIC_LEAFLET_BLOG_PUBLICATION); - - if (publication?.basePath) { - // Ensure basePath is a complete URL - redirectUrl = publication.basePath.startsWith('http') - ? publication.basePath - : `https://${publication.basePath}`; - } else { - // Use Leaflet /lish format - redirectUrl = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${PUBLIC_LEAFLET_BLOG_PUBLICATION}`; - } - } catch (error) { - console.error('Error fetching Leaflet publication:', error); - // Will check WhiteWind fallback below - } - } - - // Fallback: WhiteWind blog page (if enabled) - if (!redirectUrl && PUBLIC_ENABLE_WHITEWIND === 'true') { - redirectUrl = `https://whtwnd.com/${PUBLIC_ATPROTO_DID}`; - } - - // If we have a redirect URL, use it - if (redirectUrl) { - return new Response(null, { - status: 301, - headers: { - Location: redirectUrl, - 'Cache-Control': 'public, max-age=3600' - } - }); - } - - // No blog configured - return new Response( - 'Blog not configured. Please set PUBLIC_LEAFLET_BASE_PATH or PUBLIC_LEAFLET_BLOG_PUBLICATION in your environment variables.', - { - status: 404, - headers: { - 'Content-Type': 'text/plain; charset=utf-8' - } - } - ); - } - - return new Response(null, { status: 404 }); -};