diff --git a/src/app.css b/src/app.css index ab8247a..5f3d595 100644 --- a/src/app.css +++ b/src/app.css @@ -39,5 +39,4 @@ div.prose a { a.disabled { @apply pointer-events-none opacity-50; -} - +} \ No newline at end of file diff --git a/src/lib/server/blog-utils.ts b/src/lib/blog-utils.ts similarity index 82% rename from src/lib/server/blog-utils.ts rename to src/lib/blog-utils.ts index ef54da1..c6743bc 100644 --- a/src/lib/server/blog-utils.ts +++ b/src/lib/blog-utils.ts @@ -1,4 +1,4 @@ -import type { BlogPost } from './blog'; +import type { BlogPost, BlogPostMetadata } from './blog'; /** * Utility functions for blog content management @@ -86,7 +86,7 @@ Wrap up your blog post with a compelling conclusion. }; // Validate blog post metadata -export const validatePostMetadata = (metadata: Partial): string[] => { +export const validatePostMetadata = (metadata: Partial): string[] => { const errors: string[] = []; if (!metadata.title?.trim()) { @@ -121,8 +121,8 @@ export const validatePostMetadata = (metadata: Partial): string[] => { return errors; }; -// Search posts by keyword -export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => { +// Search posts by keyword (works with both BlogPost and BlogPostMetadata) +export const searchPosts = (posts: T[], keyword: string): T[] => { const searchTerm = keyword.toLowerCase(); return posts.filter(post => @@ -133,8 +133,8 @@ export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => { ); }; -// Group posts by year -export const groupPostsByYear = (posts: BlogPost[]): Record => { +// Group posts by year (works with both BlogPost and BlogPostMetadata) +export const groupPostsByYear = (posts: T[]): Record => { return posts.reduce((groups, post) => { const year = new Date(post.publishedAt).getFullYear().toString(); if (!groups[year]) { @@ -142,11 +142,15 @@ export const groupPostsByYear = (posts: BlogPost[]): Record } groups[year].push(post); return groups; - }, {} as Record); + }, {} as Record); }; -// Get related posts based on tags -export const getRelatedPosts = (currentPost: BlogPost, allPosts: BlogPost[], limit: number = 3): BlogPost[] => { +// Get related posts based on tags (works with metadata only for performance) +export const getRelatedPosts = ( + currentPost: BlogPost | BlogPostMetadata, + allPosts: BlogPostMetadata[], + limit: number = 3 +): BlogPostMetadata[] => { const related = allPosts .filter(post => post.slug !== currentPost.slug) .map(post => { diff --git a/src/lib/blog.ts b/src/lib/blog.ts new file mode 100644 index 0000000..449cda0 --- /dev/null +++ b/src/lib/blog.ts @@ -0,0 +1,134 @@ +import { dev } from '$app/environment'; + +export interface BlogPost { + title: string; + slug: string; + excerpt: string; + publishedAt: string; + author: string; + tags: string[]; + featured: boolean; + content?: any; // Optional since not always needed +} + +export interface BlogPostMetadata { + title: string; + slug: string; + excerpt: string; + publishedAt: string; + author: string; + tags: string[]; + featured: boolean; +} + +// Get all blog post files +const allPostFiles = import.meta.glob('$lib/posts/*.md'); + +// Parse frontmatter and extract metadata only (no content rendering) +const parsePostMetadataOnly = async (filename: string, module: any): Promise => { + const { metadata } = await module(); + + // Extract slug from filename if not provided in frontmatter + const slug = metadata.slug || filename.split('/').pop()?.replace('.md', '') || ''; + + return { + title: metadata.title || 'Untitled', + slug, + excerpt: metadata.excerpt || '', + publishedAt: metadata.publishedAt || new Date().toISOString().split('T')[0], + author: metadata.author || 'Anonymous', + tags: metadata.tags || [], + featured: metadata.featured || false + }; +}; + +// Parse frontmatter and extract metadata with content +const parsePostWithContent = async (filename: string, module: any): Promise => { + const { metadata, default: content } = await module(); + + // Extract slug from filename if not provided in frontmatter + const slug = metadata.slug || filename.split('/').pop()?.replace('.md', '') || ''; + + return { + title: metadata.title || 'Untitled', + slug, + excerpt: metadata.excerpt || '', + publishedAt: metadata.publishedAt || new Date().toISOString().split('T')[0], + author: metadata.author || 'Anonymous', + tags: metadata.tags || [], + featured: metadata.featured || false, + content: content + }; +}; + +// Get all posts metadata only (cheap operation for listing) +export const getAllPosts = async (): Promise => { + const posts: BlogPostMetadata[] = []; + + for (const [filename, module] of Object.entries(allPostFiles)) { + try { + const post = await parsePostMetadataOnly(filename, module); + // Only include posts with valid publish dates + if (post.publishedAt && new Date(post.publishedAt) <= new Date()) { + posts.push(post); + } + } catch (error) { + if (dev) { + console.error(`Error loading post metadata ${filename}:`, error); + } + } + } + + // Sort by publication date (newest first) + return posts.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()); +}; + +// Get a single post with full content by slug +export const getPostBySlug = async (slug: string): Promise => { + for (const [filename, module] of Object.entries(allPostFiles)) { + try { + // First check metadata to see if this is the right post + const metadata = await parsePostMetadataOnly(filename, module); + + if (metadata.slug === slug) { + // Only if we found the right post, load the full content + const fullPost = await parsePostWithContent(filename, module); + + // Check if post should be published + if (fullPost.publishedAt && new Date(fullPost.publishedAt) <= new Date()) { + return fullPost; + } + } + } catch (error) { + if (dev) { + console.error(`Error loading post ${filename}:`, error); + } + } + } + + return null; +}; + +// Get featured posts metadata only (cheap operation) +export const getFeaturedPosts = async (): Promise => { + const posts = await getAllPosts(); + return posts.filter(post => post.featured); +}; + +// Get posts by tag metadata only (cheap operation) +export const getPostsByTag = async (tag: string): Promise => { + const posts = await getAllPosts(); + return posts.filter(post => post.tags.includes(tag)); +}; + +// Get all unique tags (metadata only) +export const getAllTags = async (): Promise => { + const posts = await getAllPosts(); + const tagSet = new Set(); + + posts.forEach(post => { + post.tags.forEach(tag => tagSet.add(tag)); + }); + + return Array.from(tagSet).sort(); +}; \ No newline at end of file diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index a97d7e4..77ec456 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -2,7 +2,15 @@ import { page } from '$app/state'; import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte'; import ThemeSwitch from '$lib/components/ThemeSwitch.svelte'; - import { BookOpen, DollarSign, Home, LayoutDashboard, LogOut, User } from '@lucide/svelte'; + import { + BookOpen, + DollarSign, + FormInputIcon, + Home, + LayoutDashboard, + LogOut, + User + } from '@lucide/svelte'; import { Avatar } from '@skeletonlabs/skeleton-svelte'; let { data } = $props(); @@ -18,7 +26,7 @@
-