src/layouts/BlogPost.astro 2.8 K raw
1
---
2
import type { CollectionEntry } from "astro:content";
3
4
import BaseLayout from "./Base.astro";
5
import BlogHero from "@/components/blog/Hero";
6
7
interface Props {
8
	post: CollectionEntry<"post">;
9
}
10
11
const { post } = Astro.props;
12
const {
13
	data: { title, description, ogImage, publishDate },
14
	slug,
15
} = post;
16
const socialImage = ogImage ?? `/og-image/${slug}.png`;
17
const articleDate = publishDate.toISOString();
18
const { headings } = await post.render();
19
---
20
21
<script>
22
	const scrollBtn = document.getElementById("to-top-btn") as HTMLButtonElement;
23
	const targetHeader = document.getElementById("blog-hero") as HTMLDivElement;
24
25
	function callback(entries: IntersectionObserverEntry[]) {
26
		entries.forEach((entry) => {
27
			// only show the scroll to top button when the heading is out of view
28
			scrollBtn.dataset.show = (!entry.isIntersecting).toString();
29
		});
30
	}
31
32
	scrollBtn.addEventListener("click", () => {
33
		document.documentElement.scrollTo({ top: 0, behavior: "smooth" });
34
	});
35
36
	const observer = new IntersectionObserver(callback);
37
	observer.observe(targetHeader);
38
</script>
39
40
<BaseLayout meta={{ title, description, articleDate, ogImage: socialImage }}>
41
	<div class="gap-x-10 lg:flex lg:items-start">
42
		{
43
			!!headings.length && (
44
				<aside class="sticky top-20 order-2 -mr-32 hidden basis-64 lg:block">
45
					<h2 class="font-semibold">Table of Contents</h2>
46
					<ul class="mt-4 text-xs">
47
						{headings.map(({ depth, slug, text }) => (
48
							<li class="line-clamp-2 hover:text-accent">
49
								<a
50
									class={`block ${depth <= 2 ? "mt-3" : "mt-2 pl-3 text-[0.6875rem]"}`}
51
									href={`#${slug}`}
52
									aria-label={`Scroll to section: ${text}`}
53
								>
54
									<span>{"#".repeat(depth)}</span> {text}
55
								</a>
56
							</li>
57
						))}
58
					</ul>
59
				</aside>
60
			)
61
		}
62
		<article class="flex-grow break-words">
63
			<div id="blog-hero"><BlogHero content={post} /></div>
64
			<div
65
          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"
66
			>
67
				<slot />
68
			</div>
69
		</article>
70
	</div>
71
	<button
72
		id="to-top-btn"
73
		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-200 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"
74
		aria-label="Back to Top"
75
		data-show="false"
76
		><svg
77
			xmlns="http://www.w3.org/2000/svg"
78
			aria-hidden="true"
79
			focusable="false"
80
			fill="none"
81
			viewBox="0 0 24 24"
82
			stroke-width="2"
83
			stroke="currentColor"
84
			class="h-6 w-6"
85
		>
86
			<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5"></path>
87
		</svg>
88
	</button>
89
</BaseLayout>