The default blog setup from Astro is nice, but I think we can do better. I’d like to use smaller icons in the list of posts (instead of the same hero image.) I’ll also be adding dark mode, so I’ll need 2 images for each (or else something that works for both, so I’ll want to code for that.) First, dark mode. Should be easy…
Dark Mode
Well, that was easy. Right out of the Astro Docs Tutorials! Just had to tweak it a bit to work with Tailwind’s class-based dark mode.
---
// /src/components/ThemeToggle.astro
---
<button id="themeToggle">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<script is:inline>
const theme = (() => {
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
document
.getElementById("themeToggle")
.addEventListener("click", handleToggleClick);
</script>
<style>
#themeToggle {
border: 0;
background: none;
}
.sun {
fill: black;
}
.moon {
fill: transparent;
}
:global(.dark) .sun {
fill: transparent;
}
:global(.dark) .moon {
fill: white;
}
</style>
One note here for any Android users (possibly iPhone too,) if you’re testing on your mobile device in Chrome, make sure “Dark Theme” isn’t checked in the main chrome menu (3 dots in the upper right corner.) If you have that checked, your toggle isn’t going to make much difference. It will stay dark.
Hero Images
Now let’s update the frontmatter to include an image for dark mode, and while we’re at it, we’ll add something for icons too…
---
title: 'Fun with AstroJS'
description: 'Having fun with the AstroJS Framework'
pubDate: 'Mar 13 2024'
heroImage: '/img/astro-light.png'
heroImageDark: '/img/astro-dark.png'
iconImage: '/img/astro-icon-light.png'
iconImageDark: '/img/astro-icon-dark.png'
---
Now we need to update the blog post template to use these images. We’ll also add a little bit of logic to use the dark mode image when dark mode is enabled. I’m not a fan of the ternary operator in astro templates, so I’ll build the HTML as a string and just output that.
let heroHtml = "";
if (heroImage && heroImageDark) {
heroHtml = `
<img width="1020" height="510" src="${heroImage}" alt="${title}" class="block dark:hidden" />
<img width="1020" height="510" src="${heroImageDark}" alt="${title}" class="hidden dark:block" />
`;
} else if (heroImage) {
heroHtml = `<img width="1020" height="510" src="${heroImage}" alt="${title}" />`;
}
Now we can edit the BlogPost layout and replace the previous { heroImage && (<html>) }
logic…
{
heroImage && <img width={1020} height={510} src={heroImage} alt="" />;
}
…with the heroHtml
string in the template:
<div set:html={heroHtml} />
Icons
Now I just need to do the same thing for the blog list:
<ul>
{posts.map((post) => {
let iconHtml = "";
if (post.data.iconImage && post.data.iconImageDark) {
iconHtml = `
<img src="${post.data.iconImageDark}" alt="${post.data.title}" class="hidden dark:inline" />
<img src="${post.data.iconImage}" alt="${post.data.title}" width="360" height="180" class="dark:hidden" />
`;
} else if (post.data.iconImage) {
iconHtml = `
<img src="${post.data.iconImage}" alt="${post.data.title}" width="360" height="180" />
`;
}
return (
<li>
<a href={`/blog/${post.slug}/`} class="flex gap-4">
<div class="w-32 h-32" set:html={iconHtml} />
<div>
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
<p>{post.data.description}</p>
</div>
</a>
</li>
);
})}
</ul>
Hmmm, not so happy with the way that turned out. Think I’ll go ahead and use the heros for now…
let heroHtml = "";
if (post.data.heroImage && post.data.heroImageDark) {
heroHtml = `
<img src="${post.data.heroImage}" class="dark:hidden w-full h-full" transition:name="hero-${post.slug}" transition:animate={fade({duration:'2s'})}></img>
<img src="${post.data.heroImageDark}" class="dark:block hidden w-full h-full" transition:name="hero-${post.slug}-dark" transition:animate={fade({duration:'2s'})}></img>
`;
} else if (post.data.heroImage) {
heroHtml = `
<img src="${post.data.heroImage}" class="w-full h-40" transition:name="hero-${post.slug}" transition:animate={fade({duration:'2s'})}></img>
`;
}
---
<a
href={`/blog/${post.slug}`}
class="flex flex-col space-y-2 shadow-md border border-neutral-300 dark:border-neutral-800 rounded-md bg-neutral-200 dark:bg-neutral-900"
>
<div set:html={heroHtml} />
<div class="px-4 pb-4">
<h5 class="text-lg font-bold">{post.data.title}</h5>
<p class="prose flex-grow">{post.data.description}</p>
</div>
</a>
That’ll work. I think I’ll call it a day.