--- import type { CollectionEntry } from 'astro:content'; import { formatDate, formatDateMobile, processMarkdown, calculateReadingTime, getReadingTimeMobile, getPostSlug } from '../utils/markdown'; import { siteConfig, getPostCardAspectRatio } from '../config'; import { getOptimizedFormat } from '@/utils/images'; import ImageWrapper from './ImageWrapper.astro'; interface Props { post: CollectionEntry<'posts'>; featured?: boolean; eager?: boolean; context?: 'featured' | 'recent' | 'posts' | 'tags'; } const { post, featured = false, eager = false, context = 'posts' } = Astro.props; // Calculate canonical slug const slug = getPostSlug(post); // Get reading time and word count from markdown processing const { excerpt, wordCount } = processMarkdown(post.body || ''); const readingTime = calculateReadingTime(post.body || ''); const { title, description, date, tags, image: rawImage, imageAlt } = post.data; // Process Obsidian bracket syntax in image field const image = (() => { if (!rawImage) return rawImage; // Handle case where rawImage is an array (unquoted YAML syntax) let imageValue = rawImage; if (Array.isArray(rawImage)) { // If it's an array, take the first element imageValue = rawImage[0]; } // Ensure imageValue is a string before calling string methods if (typeof imageValue !== 'string') { console.warn('PostCard: imageValue is not a string:', imageValue); return imageValue; } // Check if it's Obsidian double bracket syntax if (imageValue.startsWith('[[') && imageValue.endsWith(']]')) { // Extract the content inside the double brackets return imageValue.slice(2, -2); } return imageValue; })(); // Determine if we should show the cover image based on configuration and context const shouldShowCoverImage = (() => { // Only check if image exists - hideCoverImage only affects post content, not cards if (!image) { return false; } const showCoverImages = siteConfig.postOptions.showPostCardCoverImages; switch (showCoverImages) { case 'all': return true; case 'featured': return context === 'featured' || featured; case 'home': return context === 'featured' || context === 'recent'; case 'posts': return context === 'posts' || context === 'tags'; case 'featured-and-posts': return context === 'featured' || context === 'posts' || context === 'tags' || context === 'recent' || featured; case 'none': default: return false; } })(); // No need for dynamic image import - ImageWrapper handles this ---
{image && shouldShowCoverImage && (
{ // Clean the image path by removing Obsidian brackets let cleanImagePath = image; if (!cleanImagePath || typeof cleanImagePath !== 'string') return ''; if (cleanImagePath.startsWith('[[') && cleanImagePath.endsWith(']]')) { cleanImagePath = cleanImagePath.slice(2, -2); } // Handle external URLs if (cleanImagePath.startsWith('http')) { return cleanImagePath; } // Detect folder-based vs file-based: if image path starts with 'attachments/', // it's a single-file post (shared attachments folder) const isFileBased = cleanImagePath.includes('attachments/'); if (isFileBased) { // Single-file post - remove attachments/ prefix cleanImagePath = cleanImagePath.split('attachments/').pop() || ''; } else { // Folder-based post - sync script copies images to post folder root // Remove 'images/' or 'attachments/' prefixes if present (sync script removes them) if (cleanImagePath.startsWith('images/')) { cleanImagePath = cleanImagePath.replace(/^images\//, ''); } } // Convert to WebP if applicable (sync-images.js creates WebP versions) return getOptimizedFormat(cleanImagePath); })()} basePath={(() => { // 1. External URLs - no basePath needed if (typeof image === 'string' && image.startsWith('http')) { return ''; } // 2. Clean the image path to check for attachments/ prefix let cleanImagePath = image; if (typeof cleanImagePath === 'string' && cleanImagePath.startsWith('[[') && cleanImagePath.endsWith(']]')) { cleanImagePath = cleanImagePath.slice(2, -2); } // 3. Detect folder-based vs file-based: if image path starts with 'attachments/', // it's a single-file post (shared attachments folder) if (typeof cleanImagePath === 'string' && cleanImagePath.includes('attachments/')) { // Single-file post with image in attachments folder return '/posts/attachments/'; } // 4. Folder-based post - sync script copies images to post folder root // Safety check: ensure post.id exists before using it if (post.id) { return `/posts/${post.id}/`; } else { // Fallback to attachments directory if post.id is undefined return '/posts/attachments/'; } })()} alt={imageAlt || `Featured image for post: ${title}`} class="w-full h-full group-hover:scale-102 dark:group-hover:brightness-100 transition-all duration-300" width={800} height={450} format="webp" quality={85} loading={featured || eager ? "eager" : "lazy"} fetchpriority={featured || eager ? "high" : "auto"} />
)}
{siteConfig.postOptions.readingTime && readingTime && ( <> • {getReadingTimeMobile(readingTime)} )} {siteConfig.postOptions.wordCount && wordCount !== undefined && ( <> • {wordCount === 1 ? '1 word' : `${wordCount.toLocaleString()} words`} )}

{title}

{(featured || context === 'posts' || context === 'tags') && description && (

{description}

)} {tags && tags.length > 0 && siteConfig.postOptions.tags && (
{tags.slice(0, 3).map((tag) => ( {tag} ))} {tags.length > 3 && ( + {tags.length - 3} more )}
)}