feat: add navigable table of contents to blog posts
This commit is contained in:
parent
5c1d532386
commit
05ab51aa05
7 changed files with 484 additions and 151 deletions
318
src/components/TableOfContents.astro
Normal file
318
src/components/TableOfContents.astro
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
headings: { depth: number; slug: string; text: string }[];
|
||||||
|
variant?: "mobile" | "desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headings, variant = "desktop" } = Astro.props;
|
||||||
|
|
||||||
|
// Filter to only show h2 and h3
|
||||||
|
const tocHeadings = headings.filter((h) => h.depth <= 3);
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
tocHeadings.length > 0 && (
|
||||||
|
<>
|
||||||
|
{/* Mobile TOC - Sticky Floating Button with Slide-out Panel */}
|
||||||
|
{variant === "mobile" && (
|
||||||
|
<>
|
||||||
|
{/* Floating Button */}
|
||||||
|
<button
|
||||||
|
id="mobile-toc-toggle"
|
||||||
|
class="fixed bottom-6 right-6 z-40 p-3 bg-primary dark:bg-emerald-500 text-gray-900 dark:text-gray-900 rounded-full shadow-lg hover:scale-110 transition-all duration-200 flex items-center gap-2"
|
||||||
|
aria-label="Toggle table of contents"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Slide-out Panel Overlay */}
|
||||||
|
<div
|
||||||
|
id="mobile-toc-overlay"
|
||||||
|
class="fixed inset-0 bg-black/50 z-40 opacity-0 pointer-events-none transition-opacity duration-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Slide-out Panel */}
|
||||||
|
<div
|
||||||
|
id="mobile-toc-panel"
|
||||||
|
class="fixed top-0 right-0 bottom-0 w-80 max-w-[85vw] bg-white dark:bg-gray-800 shadow-2xl z-50 transform translate-x-full transition-transform duration-300 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div class="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4 flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Table of Contents
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
id="mobile-toc-close"
|
||||||
|
class="p-2 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
aria-label="Close table of contents"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav class="p-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{tocHeadings.map((heading) => (
|
||||||
|
<li
|
||||||
|
class:list={[
|
||||||
|
{
|
||||||
|
"pl-0": heading.depth === 2,
|
||||||
|
"pl-6": heading.depth === 3,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`#${heading.slug}`}
|
||||||
|
class="mobile-toc-link block py-2.5 px-3 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-all duration-200"
|
||||||
|
data-mobile-heading={heading.slug}
|
||||||
|
>
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Desktop TOC - Sticky Sidebar */}
|
||||||
|
{variant === "desktop" && (
|
||||||
|
<aside class="sticky top-24 self-start">
|
||||||
|
<nav class="toc-nav max-h-[calc(100vh-8rem)] overflow-y-auto">
|
||||||
|
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-4 uppercase tracking-wider">
|
||||||
|
On This Page
|
||||||
|
</h2>
|
||||||
|
<ul class="space-y-2 text-sm border-l-2 border-gray-200 dark:border-gray-700">
|
||||||
|
{tocHeadings.map((heading) => (
|
||||||
|
<li
|
||||||
|
class:list={[
|
||||||
|
"border-l-2 -ml-[2px]",
|
||||||
|
{
|
||||||
|
"pl-4": heading.depth === 2,
|
||||||
|
"pl-8": heading.depth === 3,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`#${heading.slug}`}
|
||||||
|
class="toc-link block py-1 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors duration-200"
|
||||||
|
data-heading={heading.slug}
|
||||||
|
>
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
aside {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-nav {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link.active {
|
||||||
|
color: rgb(46 129 255);
|
||||||
|
border-left-color: rgb(46 129 255);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toc-link.active {
|
||||||
|
color: rgb(52 211 153);
|
||||||
|
border-left-color: rgb(52 211 153);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for desktop TOC */
|
||||||
|
.toc-nav::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-nav::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-nav::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(209 213 219);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toc-nav::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(55 65 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile TOC active state */
|
||||||
|
.mobile-toc-link.active {
|
||||||
|
background-color: rgb(243 244 246);
|
||||||
|
color: rgb(46 129 255);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .mobile-toc-link.active {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
color: rgb(52 211 153);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel open states */
|
||||||
|
#mobile-toc-panel.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-toc-overlay.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initTableOfContents() {
|
||||||
|
// Desktop TOC observer
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const id = entry.target.getAttribute("id");
|
||||||
|
const tocLink = document.querySelector(`.toc-link[data-heading="${id}"]`);
|
||||||
|
const mobileTocLink = document.querySelector(
|
||||||
|
`.mobile-toc-link[data-mobile-heading="${id}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
// Remove active class from all links
|
||||||
|
document.querySelectorAll(".toc-link").forEach((link) => {
|
||||||
|
link.classList.remove("active");
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".mobile-toc-link").forEach((link) => {
|
||||||
|
link.classList.remove("active");
|
||||||
|
});
|
||||||
|
// Add active class to current link
|
||||||
|
if (tocLink) {
|
||||||
|
tocLink.classList.add("active");
|
||||||
|
}
|
||||||
|
if (mobileTocLink) {
|
||||||
|
mobileTocLink.classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: "-100px 0px -66%",
|
||||||
|
threshold: 1.0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observe all headings
|
||||||
|
document.querySelectorAll("h2[id], h3[id]").forEach((heading) => {
|
||||||
|
observer.observe(heading);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth scroll for desktop TOC links
|
||||||
|
document.querySelectorAll(".toc-link").forEach((link) => {
|
||||||
|
link.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = link.getAttribute("href")?.slice(1);
|
||||||
|
const targetElement = document.getElementById(targetId || "");
|
||||||
|
if (targetElement) {
|
||||||
|
const offset = 100;
|
||||||
|
const elementPosition = targetElement.getBoundingClientRect().top;
|
||||||
|
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile TOC functionality
|
||||||
|
const toggleButton = document.getElementById("mobile-toc-toggle");
|
||||||
|
const closeButton = document.getElementById("mobile-toc-close");
|
||||||
|
const panel = document.getElementById("mobile-toc-panel");
|
||||||
|
const overlay = document.getElementById("mobile-toc-overlay");
|
||||||
|
|
||||||
|
function openPanel() {
|
||||||
|
panel?.classList.add("open");
|
||||||
|
overlay?.classList.add("open");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
panel?.classList.remove("open");
|
||||||
|
overlay?.classList.remove("open");
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleButton?.addEventListener("click", openPanel);
|
||||||
|
closeButton?.addEventListener("click", closePanel);
|
||||||
|
overlay?.addEventListener("click", closePanel);
|
||||||
|
|
||||||
|
// Smooth scroll for mobile TOC links
|
||||||
|
document.querySelectorAll(".mobile-toc-link").forEach((link) => {
|
||||||
|
link.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = link.getAttribute("href")?.slice(1);
|
||||||
|
const targetElement = document.getElementById(targetId || "");
|
||||||
|
if (targetElement) {
|
||||||
|
const offset = 80;
|
||||||
|
const elementPosition = targetElement.getBoundingClientRect().top;
|
||||||
|
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the panel after clicking
|
||||||
|
closePanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close panel on escape key
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && panel?.classList.contains("open")) {
|
||||||
|
closePanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
initTableOfContents();
|
||||||
|
|
||||||
|
// Re-initialize after view transitions (if using Astro view transitions)
|
||||||
|
document.addEventListener("astro:after-swap", initTableOfContents);
|
||||||
|
</script>
|
||||||
|
|
@ -31,7 +31,7 @@ This page is a small recap of my story. Nothing special, just me.
|
||||||
## Childhood Nostalgia
|
## Childhood Nostalgia
|
||||||
|
|
||||||
<div class="float-right img-medium">
|
<div class="float-right img-medium">
|
||||||
<Image src={meBaby} alt="Super young geek with an Apple II" class="float-right img-medium" width={400} height={300} quality={85} format="webp" />
|
<Image src={meBaby} alt="Super young geek with an Apple II" class="float-right img-medium" width={380} height={450} quality={75} format="webp" />
|
||||||
<em class="text-sm block mt-2">Super young software geek with an Apple II</em>
|
<em class="text-sm block mt-2">Super young software geek with an Apple II</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ I grew up in Ispica, in the south of Sicily. My days were simple: school, videog
|
||||||
Ispica is slow, warm, and beautiful. When I was a kid I didn’t see it that way. I wanted to escape. I was dreaming about big cities, more people, more things happening, more opportunities.
|
Ispica is slow, warm, and beautiful. When I was a kid I didn’t see it that way. I wanted to escape. I was dreaming about big cities, more people, more things happening, more opportunities.
|
||||||
|
|
||||||
<div class="float-left img-medium">
|
<div class="float-left img-medium">
|
||||||
<Image src={pokemon} alt="Pokemon Yellow and Game Boy Advance" class="float-left img-medium" width={400} height={300} quality={85} format="webp" />
|
<Image src={pokemon} alt="Pokemon Yellow and Game Boy Advance" class="float-left img-medium" width={430} height={300} quality={85} format="webp" />
|
||||||
<em class="text-sm block mt-2">Pokemon Yellow and Game Boy Advance</em>
|
<em class="text-sm block mt-2">Pokemon Yellow and Game Boy Advance</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ At 17 I also discovered Magic: The Gathering. It became another obsession for a
|
||||||
I studied Computer Science at the University of Pisa. It was not a straight path.
|
I studied Computer Science at the University of Pisa. It was not a straight path.
|
||||||
|
|
||||||
<div class="float-right img-medium">
|
<div class="float-right img-medium">
|
||||||
<Image src={meCC} alt="Me burning out studying" class="float-right img-medium" width={400} height={300} quality={85} format="webp" />
|
<Image src={meCC} alt="Me burning out studying" class="float-right img-medium" width={400} height={450} quality={85} format="webp" />
|
||||||
<em class="text-sm block mt-2">Me burning out studying Computability and Complexity exam</em>
|
<em class="text-sm block mt-2">Me burning out studying Computability and Complexity exam</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ That period also started my love for traveling. Seeing different cultures in rea
|
||||||
## Embarking on Hackathon Adventures
|
## Embarking on Hackathon Adventures
|
||||||
|
|
||||||
<div class="float-right img-small">
|
<div class="float-right img-small">
|
||||||
<Image src={meMoverio} alt="Me wearing moverio smart glasses" class="float-right img-small" width={300} height={300} quality={85} format="webp" />
|
<Image src={meMoverio} alt="Me wearing moverio smart glasses" class="float-right img-small" width={300} height={350} quality={85} format="webp" />
|
||||||
<em class="text-sm block mt-2">Me wearing moverio smart glasses</em>
|
<em class="text-sm block mt-2">Me wearing moverio smart glasses</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ That experience made me addicted to hackathons. After that I joined other events
|
||||||
## Erasmus Project in Valencia
|
## Erasmus Project in Valencia
|
||||||
|
|
||||||
<div class="float-right img-large">
|
<div class="float-right img-large">
|
||||||
<Image src={valenciaTuria} alt="Beautiful sunny day in Valencia" class="float-right img-large" width={600} height={400} quality={80} format="webp" />
|
<Image src={valenciaTuria} alt="Beautiful sunny day in Valencia" class="float-right img-large" width={600} height={350} quality={80} format="webp" />
|
||||||
<em class="text-sm block mt-2">Beautiful sunny day in Valencia</em>
|
<em class="text-sm block mt-2">Beautiful sunny day in Valencia</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ At some point I decided to go back to Sicily.
|
||||||
Not as a “I give up” move. More like: I want a different balance.
|
Not as a “I give up” move. More like: I want a different balance.
|
||||||
|
|
||||||
<div class="float-left img-medium">
|
<div class="float-left img-medium">
|
||||||
<Image src={remote} alt="Working remote watching the sea" class="float-left img-medium" width={300} height={225} quality={75} format="webp" />
|
<Image src={remote} alt="Working remote watching the sea" class="float-left img-medium" width={300} height={200} quality={75} format="webp" />
|
||||||
<em class="text-sm block mt-2">Working remote watching the sea</em>
|
<em class="text-sm block mt-2">Working remote watching the sea</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -148,13 +148,14 @@ Time here feels different. You can actually breathe. You can have “nothing spe
|
||||||
|
|
||||||
Family is a big part of my life here. And I also started a side project with my sister (she’s an agronomist): we planted a small vineyard near the sea and we started producing our own wine.
|
Family is a big part of my life here. And I also started a side project with my sister (she’s an agronomist): we planted a small vineyard near the sea and we started producing our own wine.
|
||||||
|
|
||||||
That project became [www.netum.it](https://netum.it/). It’s small (1 hectare), limited bottles, but it’s something we built together and I love it.
|
|
||||||
|
|
||||||
<div class="float-right img-small">
|
<div class="float-right img-small">
|
||||||
<Image src={wine} alt="The wine produced Zia Lina" class="float-right img-small" width={300} height={300} quality={85} format="webp" />
|
<Image src={wine} alt="The wine produced Zia Lina" class="float-right img-small" width={300} height={400} quality={85} format="webp" />
|
||||||
<em class="text-sm block mt-2">The wine produced "Zia Lina"</em>
|
<em class="text-sm block mt-2">The wine produced "Zia Lina"</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
That project became [www.netum.it](https://netum.it/). It’s small (1 hectare), limited bottles, but it’s something we built together and I love it.
|
||||||
|
|
||||||
|
|
||||||
And yes: food. Sicily is crazy for food. It’s not even a “food culture”, it’s basically a religion.
|
And yes: food. Sicily is crazy for food. It’s not even a “food culture”, it’s basically a religion.
|
||||||
|
|
||||||
*(No food photos here. I’m not a food blogger. But trust me.)*
|
*(No food photos here. I’m not a food blogger. But trust me.)*
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ const initialPosts: BlogPost[] = sortedPosts.slice(0, 6);
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
{post.data.tags.slice(0, 3).map((tag: string) => (
|
{post.data.tags.map((tag: string) => (
|
||||||
<a
|
<a
|
||||||
href={`/blog/tag/${tag}`}
|
href={`/blog/tag/${tag}`}
|
||||||
class="px-2 py-1 bg-secondary/10 dark:bg-primary/20 text-gray-800 dark:text-gray-200 text-xs rounded-lg font-medium hover:bg-secondary hover:text-white dark:hover:bg-primary dark:hover:text-gray-900 transition-all duration-200"
|
class="px-2 py-1 bg-secondary/10 dark:bg-primary/20 text-gray-800 dark:text-gray-200 text-xs rounded-lg font-medium hover:bg-secondary hover:text-white dark:hover:bg-primary dark:hover:text-gray-900 transition-all duration-200"
|
||||||
|
|
@ -238,7 +238,6 @@ const initialPosts: BlogPost[] = sortedPosts.slice(0, 6);
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
${post.data.tags
|
${post.data.tags
|
||||||
.slice(0, 3)
|
|
||||||
.map(
|
.map(
|
||||||
(tag) =>
|
(tag) =>
|
||||||
`<a href="/blog/tag/${tag}" class="px-2 py-1 bg-secondary/10 dark:bg-primary/20 text-gray-800 dark:text-gray-200 text-xs rounded-lg font-medium hover:bg-secondary hover:text-white dark:hover:bg-primary dark:hover:text-gray-900 transition-all duration-200">${tag}</a>`
|
`<a href="/blog/tag/${tag}" class="px-2 py-1 bg-secondary/10 dark:bg-primary/20 text-gray-800 dark:text-gray-200 text-xs rounded-lg font-medium hover:bg-secondary hover:text-white dark:hover:bg-primary dark:hover:text-gray-900 transition-all duration-200">${tag}</a>`
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Image } from "astro:assets";
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import Navbar from "../../components/Navbar.astro";
|
import Navbar from "../../components/Navbar.astro";
|
||||||
import Footer from "../../components/Footer.astro";
|
import Footer from "../../components/Footer.astro";
|
||||||
|
import TableOfContents from "../../components/TableOfContents.astro";
|
||||||
import "../../styles/prose.css";
|
import "../../styles/prose.css";
|
||||||
import mePhoto from "../../assets/photos/me.png";
|
import mePhoto from "../../assets/photos/me.png";
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ export async function getStaticPaths() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entry }: { entry: BlogPost } = Astro.props;
|
const { entry }: { entry: BlogPost } = Astro.props;
|
||||||
const { Content } = await entry.render();
|
const { Content, headings } = await entry.render();
|
||||||
|
|
||||||
// Extract image src for meta tags (Open Graph expects a URL string)
|
// Extract image src for meta tags (Open Graph expects a URL string)
|
||||||
const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
||||||
|
|
@ -36,7 +37,9 @@ const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
||||||
>
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="min-h-screen pt-16 bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
|
<div class="min-h-screen pt-16 bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
|
||||||
<article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-12">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-12">
|
||||||
|
<div class="flex gap-8 items-start">
|
||||||
|
<article class="flex-1 max-w-4xl min-w-0">
|
||||||
<header class="mb-8 md:mb-12">
|
<header class="mb-8 md:mb-12">
|
||||||
<time
|
<time
|
||||||
datetime={entry.data.pubDate.toISOString()}
|
datetime={entry.data.pubDate.toISOString()}
|
||||||
|
|
@ -94,6 +97,11 @@ const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Mobile TOC -->
|
||||||
|
<div class="xl:hidden">
|
||||||
|
<TableOfContents headings={headings} variant="mobile" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl md:rounded-2xl shadow-xl p-6 sm:p-8 md:p-12 lg:p-16 transition-colors duration-200"
|
class="bg-white dark:bg-gray-800 rounded-xl md:rounded-2xl shadow-xl p-6 sm:p-8 md:p-12 lg:p-16 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
|
|
@ -120,9 +128,9 @@ const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
||||||
Lorenzo Iovino
|
Lorenzo Iovino
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm md:text-base text-gray-700 dark:text-gray-300 mb-4">
|
<p class="text-sm md:text-base text-gray-700 dark:text-gray-300 mb-4">
|
||||||
I write software and I also work on a small vineyard in Sicily. In code, trees grow
|
I write software and I also work on a small vineyard in Sicily. In code, trees
|
||||||
downward from the root. In real life, trees grow upward from the roots. I spend way
|
grow downward from the root. In real life, trees grow upward from the roots. I
|
||||||
too much time thinking about this.
|
spend way too much time thinking about this.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4 justify-center sm:justify-start">
|
<div class="flex gap-4 justify-center sm:justify-start">
|
||||||
<a
|
<a
|
||||||
|
|
@ -176,6 +184,13 @@ const heroImageSrc = entry.data.heroImage?.src || mePhoto.src;
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<!-- Desktop TOC Sidebar -->
|
||||||
|
<div class="hidden xl:block flex-shrink-0 self-start sticky top-24">
|
||||||
|
<TableOfContents headings={headings} variant="desktop" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ const displayTag: string = tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
{post.data.tags.slice(0, 3).map((postTag: string) => (
|
{post.data.tags.map((postTag: string) => (
|
||||||
<a
|
<a
|
||||||
href={`/blog/tag/${postTag}`}
|
href={`/blog/tag/${postTag}`}
|
||||||
class={`px-2 py-1 text-xs rounded-lg font-medium transition-all duration-200 ${
|
class={`px-2 py-1 text-xs rounded-lg font-medium transition-all duration-200 ${
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@apply text-xl md:text-2xl lg:text-3xl;
|
@apply text-lg md:text-xl lg:text-2xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h3 {
|
.prose h3 {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.35rem !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
color: #111827 !important;
|
color: #111827 !important;
|
||||||
margin-top: 3rem !important;
|
margin-top: 3rem !important;
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.prose h3 {
|
.prose h3 {
|
||||||
font-size: 1.875rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue