--- import { getEmDashEntry, getEmDashCollection, getEntryTerms, getTermsForEntries, getSeoMeta, decodeSlug, getSiteSettings, } from "emdash"; import { Image, PortableText, Comments, CommentForm, WidgetArea, } from "emdash/ui"; import Base from "../../layouts/Base.astro"; import PostCard from "../../components/PostCard.astro"; import { getReadingTime } from "../../utils/reading-time"; import { resolveBlogSiteIdentity } from "../../utils/site-identity"; const slug = decodeSlug(Astro.params.slug); if (!slug) { return Astro.redirect("/404"); } const { entry: post, cacheHint } = await getEmDashEntry("posts", slug); if (!post) { return Astro.redirect("/404"); } Astro.cache.set(cacheHint); // Get featured image URL for OG fallback // The image may have src (external) or meta.storageKey (local) function getImageUrl(img: unknown): string | undefined { if (!img || typeof img !== "object") return undefined; const image = img as Record; // Check for direct src if (typeof image.src === "string" && image.src) { return image.src.startsWith("http") ? image.src : `${Astro.url.origin}${image.src}`; } // Build from storageKey for local images const meta = image.meta as Record | undefined; const storageKey = (typeof meta?.storageKey === "string" ? meta.storageKey : undefined) || (typeof image.id === "string" ? image.id : undefined); if (storageKey) { return `${Astro.url.origin}/_emdash/api/media/file/${storageKey}`; } return undefined; } const featuredImageUrl = getImageUrl(post.data.featured_image); const { siteTitle } = resolveBlogSiteIdentity(await getSiteSettings()); // Generate SEO meta from content const seo = getSeoMeta(post, { siteTitle, siteUrl: Astro.url.origin, path: `/posts/${slug}`, defaultOgImage: featuredImageUrl, }); // Bylines are already hydrated by getEmDashEntry const bylines = post.data.bylines ?? []; // Get reading time const readingTime = getReadingTime(post.data.content); // Fetch this post's tags and the related-posts list in parallel — they're // independent queries, so running them concurrently halves the round-trip // cost on remote databases. // Note: post.id is the slug, post.data.id is the database ULID. const [tags, { entries: recentPosts }] = await Promise.all([ getEntryTerms("posts", post.data.id, "tag"), // Fetch a few extra in case the current post is among them getEmDashCollection("posts", { orderBy: { published_at: "desc" }, limit: 4, }), ]); const otherPosts = recentPosts.filter((p) => p.id !== post.id).slice(0, 3); // Single batched query for related-posts tags, rather than one // getEntryTerms() call per related post. const otherTagsByEntry = await getTermsForEntries( "posts", otherPosts.map((p) => p.data.id), "tag", ); const otherPostsWithTags = otherPosts.map((p) => ({ post: p, tags: otherTagsByEntry.get(p.data.id) ?? [], bylines: p.data.bylines ?? [], })); const publishDate = post.data.publishedAt?.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }) ?? null; ---
{/* Hero: Full-width featured image */} { post.data.featured_image && (
) } {/* Three-column layout */}
{/* Left gutter: Meta information */} {/* Main content */}

{post.data.title}

{ post.data.excerpt && (

{post.data.excerpt}

) }
{/* Right gutter: TOC + Sidebar widgets */}
{ otherPostsWithTags.length > 0 && ( ) }