Customizing astro-erudite: What I Added to the Base Template
A walkthrough of every feature added to astro-erudite v2, from the bento homepage to Giscus comments.
astro-erudite v2 is a minimal, opinionated blogging template. Out of the box it gives you a blog, projects, an about page, and a sidebar. This post covers everything I added on top of that foundation.
Bento grid homepage
A bento grid layout — a two-column card-based interface that surfaces content at a glance.
The grid is built with BentoCard.astro, a generic card wrapper that supports column and row spanning:
<BentoCard span={2} href="/about" class="bento-hero"> <img src="..." alt="..." class="bento-hero-avatar" /> <div class="bento-hero-name">Vanam-san</div> <div class="bento-hero-subtitle">Tinkerer</div> <p class="bento-hero-bio" data-reveal>...</p> <span class="bento-link">View full profile →</span></BentoCard>The grid contains five card types:
- Hero card (full width) — avatar, name, subtitle, and a bio with a sequential character-reveal animation
- Latest posts — the two most recent blog entries with titles, descriptions, and dates
- Latest gallery — a card with an auto-cycling image slideshow that crossfades every 3 seconds
- Projects — the latest project with a link to the full projects page
- Time and weather (full width) — live clock, local weather via wttr.in, and timezone info
All cards share the same hover treatment: border highlight, background darkening, box shadow, and a 2px upward translate. The gallery card additionally scales its active slide to 1.05x on hover.
The entire grid collapses to a single column on viewports narrower than 40rem.
Umami analytics
Umami — a privacy-focused, cookieless analytics tool.
Configuration lives in consts.ts:
export const UMAMI = { websiteId: import.meta.env.UMAMI_WEBSITE_ID || "", host: import.meta.env.UMAMI_HOST || "cloud.umami.is",}The tracking script is injected conditionally in MetaHead.astro — it only loads if UMAMI_WEBSITE_ID is set:
{UMAMI.websiteId && ( <script is:inline defer src={`https://${UMAMI.host}/script.js`} data-website-id={UMAMI.websiteId} />)}This means the template works identically without analytics configured. No cookies, no personal data collection, no GDPR banner needed.
Giscus comments
Giscus — uses GitHub Discussions as a backend.
The Giscus.astro component handles three things:
- Loading — it creates a
<script>tag pointing togiscus.app/client.jswith the repo, category, and mapping configuration as data attributes. - Theme syncing — a
MutationObserverwatchesdata-themeon<html>and posts asetConfigmessage to the Giscus iframe whenever the theme changes. - Re-initialization — on View Transitions page loads (
astro:page-loadandastro:after-swap), it tears down the old iframe and script, then reloads fresh.
The component is conditionally rendered on blog posts:
{GISCUS.repo && GISCUS.repoId && GISCUS.categoryId && <Giscus />}Like Umami, it requires no configuration to function — just set three environment variables and comments appear.
Gallery system
A complete gallery feature with:
- Content collection —
galleryentries with a title, description, date, cover image, and an array of photos - Listing page — a responsive grid of
GalleryCardcomponents at/gallery - Detail pages — individual gallery pages with a photo grid, lightbox, and optional Markdown content
- Lightbox — a full-screen overlay with prev/next navigation, keyboard support, and a photo counter
- Homepage integration — the latest gallery appears in the bento grid with an auto-cycling slideshow
The gallery card styling mirrors the blog and project cards: border, background, hover effects, and image scaling.
Setup as a blog post
A dedicated setup blog post at /blog/my-setup showcasing hardware and software tools, instead of a standalone /setup page. This keeps all content in the blog collection and makes it easier to maintain alongside other posts.
Scroll-to-top button
A floating variant of ScrollToTop.astro that renders as a fixed-position button in the bottom-right corner on mobile. It appears after scrolling past 50% of the viewport height and uses backdrop-filter: blur() for a translucent background.
<ScrollToTop floating />This component is used on the homepage, blog posts, and gallery detail pages.
Blog and project card hover effects
Blog and project cards were flat list items with no visual containment. I added card-like styling to match the gallery cards:
border: 1.5px solid var(--border)withborder-radius: var(--radius-2xl)background-color: color-mix(in oklab, var(--muted) 20%, transparent)- Hover state: border brightens, background darkens, box shadow appears, card translates up 2px
- Image containers get
overflow: hiddenandborder-radius: var(--radius-xl), with images scaling to 1.05x on hover
Sequential text reveal animation
The hero card bio on the homepage uses a character-by-character reveal animation. Each character is wrapped in a <span> with opacity: 0.2, then sequentially transitioned to opacity: 1 over 3 seconds using setTimeout delays.
const delayPerChar = totalDuration / spans.lengthspans.forEach((span, i) => { setTimeout(() => { span.classList.add('revealed') }, i * delayPerChar)})The animation re-runs on View Transitions page swaps.
Gallery slideshow
The bento homepage gallery card includes a slideshow that crossfades between the first four photos of the latest gallery. Each slide is absolutely positioned with opacity: 0, and a 3-second interval cycles the active class:
setInterval(() => { slides[currentIndex].classList.remove('active') currentIndex = (currentIndex + 1) % slides.length slides[currentIndex].classList.add('active')}, 3000)The active slide additionally scales to 1.05x on card hover.
Weather widget
The time/weather card on the homepage fetches live weather data from wttr.in — a free, API-key-less weather service. It maps WMO weather codes to emojis and displays temperature and description.
const res = await fetch("https://wttr.in/?format=j1")const data = await res.json()const current = data.current_condition[0]The clock updates every second using setInterval.
Theme system — Ocean Depths + HANCORE archive
The base erudite template uses a simple light/dark scheme with neutral grays. I replaced it with a palette switcher that defaults to Moodpeak: a built-in Ocean Depths theme (cyan/lume accents, light-dark() auto mode) plus 28 palettes adapted from the HANCORE-linux Omarchy theme archive — each mapped from its upstream colors.toml onto the site tokens:
:root[data-palette="banish"] { --background: #0c0b0d; --foreground: #d8d1cc; --primary: #ff2fb3; /* upstream accent */ --secondary: #6c70e0; /* upstream blue */ --muted: #19181a; /* upstream lighter_background */}The palette icon in the sidebar opens the theme menu. Each entry shows a color dot (the palette accent), the theme name, and a tick marking the selection. The menu drops upward on desktop (the trigger sits at the bottom of the sidebar), is right-anchored on mobile, and supports full keyboard navigation (arrows/Escape). The choice persists to localStorage, applies pre-paint to avoid a flash, and each palette’s designed light/dark mode is mirrored to data-theme so the circuit backdrop and Giscus comments follow it.
To add a theme: append an entry to THEMES in src/lib/themes.ts, add a matching :root[data-palette="<id>"] block in src/styles/themes.css — the menu renders from the registry, no component changes needed.
Glow effects are layered on interactive elements:
--glow-primary: 0 0 20px color-mix(in oklab, var(--cyan-9) 25%, transparent);--glow-secondary: 0 0 20px color-mix(in oklab, var(--lume-9) 20%, transparent);Cards, sidebar links, and the footer gradient all reference these tokens, keeping the visual language consistent.
Typography — terminal-inspired pairing
The base template uses a single sans-serif stack everywhere. I split typography into three roles, chosen to match the terminal/desktop-theme aesthetic:
- Display (headings): Space Grotesk — a techy geometric sans, loaded via Google Fonts
- Body: Inter — a neutral variable sans for readability, loaded via Google Fonts
- Mono: JetBrains Mono — the developer monospace for code and metadata, loaded via Google Fonts
Space Grotesk is applied via a --font-display custom property and used on the sidebar brand, page titles, and bento hero name. The heading scale uses tighter tracking (-0.02em on large sizes) and a heavier weight (700) to create contrast against the body text.
--font-display: "Space Grotesk", "Inter", system-ui, sans-serif;The fonts are preconnected in MetaHead.astro and defined in fonts.css.
Vertical bar hover effect on list cards
Blog, gallery, and project cards all share a list-item layout (date | title | tags | meta). I added a 2px vertical accent bar on the left edge that animates in on hover — inspired by the Blowfish theme used on vanam.dev.
Each <li> gets position: relative and a ::before pseudo-element:
li::before { content: ""; position: absolute; left: 0; top: var(--space-2xs); bottom: var(--space-2xs); width: 2px; background: var(--primary); opacity: 0; transform: scaleY(0); transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);}
li:hover::before { opacity: 1; transform: scaleY(1);}The bar scales from center using scaleY, giving a smooth reveal effect. The same pattern is applied to bento card post/project lists using the secondary (cosmic blue) accent.
Unified tag system
The base v2 template has no tag infrastructure. I built a unified tag system that spans all three collections:
getAllTags()incontent.tscollects tags from blog, gallery, and projects into a singleMap<string, TagGroup>- Tags index at
/tagsshows all tags with total counts across collections - Tag detail pages at
/tags/[id]render matching items grouped by type (blog, gallery, projects) - All tags are normalized to lowercase for consistency
type TagGroup = { blog: CollectionEntry<"blog">[] gallery: CollectionEntry<"gallery">[] projects: CollectionEntry<"projects">[]}Tags are clickable on every card and on the tag detail pages, creating cross-collection navigation.
Gallery list layout with year sorting
The gallery started as a grid of cards with cover images. I converted it to a list layout matching the blog and project cards:
- Date, title, tags, photo count, and optional description in a grid
- Galleries sorted by year with year group headings
- Vertical bar hover effect matching other list cards
- Tags normalized to lowercase on display
The getGalleriesByYear() utility groups galleries by year and returns a Map<number, Gallery[]> sorted descending.
Blog card alignment
The blog card was originally a different layout from gallery and project cards. I aligned all three to share the same structure:
| Column | Content |
|---|---|
| 1 (4.5rem) | Date in monospace |
| 2 (1fr) | Title as link |
| Full width | Tags in mono |
| Full width | Meta (words/time or photo count) |
Blog cards show words · min read, gallery cards show N photos, and project cards show an optional description. All use the same grid template, border treatment, and hover effects.
Summary
| Feature | Added |
|---|---|
| Homepage | Bento grid with 5 card types |
| Analytics | Umami (cookieless) |
| Comments | Giscus (GitHub Discussions) |
| Galleries | Full gallery with lightbox |
| Setup | Blog post with hardware/software showcase |
| Scroll to top | Floating mobile button |
| Card hover effects | Blog, projects, vertical bar accent |
| Reading time + word count | Blog listing cards too |
| Text reveal animation | Hero card bio |
| Gallery slideshow | Auto-cycling bento card |
| Weather widget | Live wttr.in data |
| Color theme | Ocean Depths + 28 HANCORE palettes, switcher menu |
| Typography | Space Grotesk display + Inter body + JetBrains Mono |
| Tag system | Unified cross-collection tags |
| Gallery layout | List with year sorting |
Every addition follows the same principle as the base template: minimal JavaScript, native CSS, and no unnecessary dependencies. The Giscus and Umami integrations are the only external services, and both are optional — the site works fully without them configured.