src/layouts/BlogPost.astro 3.1 K raw
1
---
2
import type { CollectionEntry } from "astro:content";
3
import { render } from "astro:content";
4
import { getImage } from "astro:assets";
5
6
import BaseLayout from "./Base.astro";
7
import BlogHero from "@/components/blog/Hero.astro";
8
9
interface Props {
10
	post: CollectionEntry<"post">;
11
}
12
13
const { post } = Astro.props;
14
const {
15
	data: { title, description, ogImage, publishDate, atUri },
16
} = post;
17
18
let socialImage: string;
19
if (!ogImage) {
20
	socialImage = "/social-card.png";
21
} else if (typeof ogImage === "string") {
22
	socialImage = ogImage;
23
} else {
24
	const optimized = await getImage({
25
		src: ogImage,
26
		format: "png",
27
		width: 1200,
28
	});
29
	socialImage = optimized.src;
30
}
31
const articleDate = publishDate.toISOString();
32
const { headings } = await render(post);
33
---
34
35
<script>
36
	const scrollBtn = document.getElementById("to-top-btn") as HTMLButtonElement;
37
	const targetHeader = document.getElementById("blog-hero") as HTMLDivElement;
38
39
	function callback(entries: IntersectionObserverEntry[]) {
40
		entries.forEach((entry) => {
41
			// only show the scroll to top button when the heading is out of view
42
			scrollBtn.dataset.show = (!entry.isIntersecting).toString();
43
		});
44
	}
45
46
	scrollBtn.addEventListener("click", () => {
47
		document.documentElement.scrollTo({ top: 0, behavior: "smooth" });
48
	});
49
50
	const observer = new IntersectionObserver(callback);
51
	observer.observe(targetHeader);
52
</script>
53
54
<BaseLayout meta={{ title, description, articleDate, ogImage: socialImage, atUri }}>
55
	<div class="gap-x-10 lg:flex lg:items-start">
56
		{
57
			!!headings.length && (
58
				<aside class="sticky top-20 order-2 -mr-32 hidden basis-64 lg:block">
59
					<h2 class="font-semibold">Table of Contents</h2>
60
					<ul class="mt-4 text-xs">
61
						{headings.map(({ depth, slug, text }) => (
62
							<li class="line-clamp-2 hover:text-accent">
63
								<a
64
									class={`block ${depth <= 2 ? "mt-3" : "mt-2 pl-3 text-[0.6875rem]"}`}
65
									href={`#${slug}`}
66
									aria-label={`Scroll to section: ${text}`}
67
								>
68
									<span>{"#".repeat(depth)}</span> {text}
69
								</a>
70
							</li>
71
						))}
72
					</ul>
73
				</aside>
74
			)
75
		}
76
		<article class="flex-grow break-words">
77
			<div id="blog-hero"><BlogHero content={post} /></div>
78
			<div
79
				class="prose prose-sm prose-cactus mt-12 prose-headings:font-semibold prose-headings:before:absolute prose-headings:before:-ml-4 prose-headings:before:text-accent prose-headings:before:content-['#'] prose-th:before:content-none"
80
			>
81
				<slot />
82
			</div>
83
		</article>
84
	</div>
85
	<button
86
		id="to-top-btn"
87
		class="z-90 fixed bottom-8 right-4 flex h-10 w-10 translate-y-28 items-center justify-center rounded-full border-2 border-transparent bg-zinc-900 text-3xl opacity-0 transition-all duration-300 hover:border-zinc-400 data-[show=true]:translate-y-0 data-[show=true]:opacity-100 dark:bg-zinc-700 sm:right-8 sm:h-12 sm:w-12"
88
		aria-label="Back to Top"
89
		data-show="false"
90
		><svg
91
			xmlns="http://www.w3.org/2000/svg"
92
			aria-hidden="true"
93
			focusable="false"
94
			fill="none"
95
			viewBox="0 0 24 24"
96
			stroke-width="2"
97
			stroke="currentColor"
98
			class="h-6 w-6"
99
		>
100
			<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5"></path>
101
		</svg>
102
	</button>
103
</BaseLayout>