Changelog System Refactor: Layouts, Components, and Client-Side View Toggling
Extracted changelog page variants into reusable layouts with client-side view toggling. Created Button--Default component, ChangelogViewToggler, and three PageHeader variants for the design system.
Changelog - 2025-12-25 (#6)
Dark Matter Site: Changelog System Refactor
Overview
Refactored the three changelog page variants (Timeline, Cards, Releases) into a unified system with reusable layouts and client-side view toggling. The previous implementation had three separate pages (index.astro, variant-1.astro, variant-3.astro) with duplicated code. Now all three views live on a single page with instant client-side switching.
Key insight: By rendering all three layouts on initial page load (hidden via CSS), we achieve instant view switching without any server round-trips or flash of unstyled content.
Key deliverables:
Three Changelog Layouts: Timeline, CardGrid, and Releases as standalone layout components
Button--Default Component: Exact replica of the variant-tab styling for reuse
ChangelogViewToggler: View switching with share URL functionality
Three PageHeader Variants: Centered, SplitNav, and Compact header components
No-Flash View Switching: CSS
data-initial-viewpattern for SSR-friendly toggling
Why This Refactor?
| Before | After |
| 3 separate pages with duplicated rendering logic | 1 page with 3 reusable layouts |
| URL changes on view switch (full page load) | Instant client-side toggle, URL stays or updates |
| Copy-paste button styling across pages | Single Button--Default component |
| Hardcoded headers per page | Reusable PageHeader--* components |
Architecture
The Layout System
src/layouts/changelog/
├── TimelineLayout.astro # Vertical timeline with month grouping
├── CardGridLayout.astro # Grid cards with category filtering
└── ReleasesLayout.astro # GitHub-style collapsible releasesEach layout receives the same props and renders its own visualization:
interface Props {
entries: CollectionEntry<'changelog'>[];
groupedByMonth?: Map<string, CollectionEntry<'changelog'>[]>;
}Client-Side View Toggling Pattern
The key innovation is a CSS-first approach that avoids flash:
<!-- Initial state set via CSS data attribute (SSR-safe) -->
<div class="views-container" data-initial-view="timeline">
<TimelineLayout ... /> <!-- shown via CSS when data-initial-view="timeline" -->
<CardGridLayout ... /> <!-- hidden via CSS -->
<ReleasesLayout ... /> <!-- hidden via CSS -->
</div>
<style>
/* CSS handles initial state - no JS needed for first paint */
[data-initial-view="timeline"] .timeline-view { display: block; }
[data-initial-view="timeline"] .cards-view { display: none; }
[data-initial-view="timeline"] .releases-view { display: none; }
</style>
<script>
// JS takes over after hydration - removes data-initial-view
container.removeAttribute('data-initial-view');
// Now JS controls visibility via inline styles
</script>Why this matters: The CSS ensures correct initial render without waiting for JavaScript. Once JS hydrates, it removes the attribute and takes full control.
Components Created
Button--Default
A foundational button component matching the exact styling of the changelog variant tabs:
<ButtonDefault href="/changelog" active={isActive}>
<svg slot="icon">...</svg>
Timeline
</ButtonDefault>Features:
Works as both
<a>(with href) and<button>(without href)Active state styling with violet glow
Icon slot for inline SVGs
Data attribute pass-through for JS interaction
ChangelogViewToggler
Renders the three view buttons with share functionality:
<ChangelogViewToggler initialView="timeline" />Features:
Three toggle buttons (Timeline, Cards, Releases)
Share button that copies URL with
?view=parameterToast notification on copy
URL update on view change (without page reload)
PageHeader Variants
Three header components for different page contexts:
| Component | Use Case | Features |
PageHeader--Centered | Landing pages, announcements | Gradient title, badge, centered layout |
PageHeader--SplitNav | List pages, dashboards | Split layout, counter badge, filter slot |
PageHeader--Compact | Simple content pages | Minimal, GitHub-style |
Technical Details
Sorting Fix
Fixed incorrect ordering of same-day changelog entries:
// Before: Only sorted by date
entries.sort((a, b) => b.data.date - a.data.date);
// After: Secondary sort by entry ID (descending) for same-day entries
entries.sort((a, b) => {
const dateCompare = b.data.date.getTime() - a.data.date.getTime();
if (dateCompare !== 0) return dateCompare;
return b.id.localeCompare(a.id); // _05 comes before _04 before _03...
});Releases View Filter
The Releases layout now properly filters to only show entries from the releases/ folder:
const releaseEntries = entries.filter(entry =>
entry.id.startsWith('releases/')
);Header Text Clipping Fix
Fixed the "g" descender being cut off in gradient titles caused by bg-clip-text:
<!-- Added pb-1 for descender clearance -->
<h1 class="... bg-clip-text pb-1">Changelog</h1>File Summary
New Files (8)
| File | Lines | Purpose |
src/layouts/changelog/TimelineLayout.astro | 180 | Timeline view with month grouping |
src/layouts/changelog/CardGridLayout.astro | 280 | Card grid with category filters |
src/layouts/changelog/ReleasesLayout.astro | 190 | GitHub-style releases |
src/components/basics/buttons/Button--Default.astro | 120 | Reusable styled button |
src/components/ui/ChangelogViewToggler.astro | 180 | View toggle with share |
src/components/page-headers/PageHeader--Centered.astro | 110 | Centered hero header |
src/components/page-headers/PageHeader--SplitNav.astro | 70 | Split nav header |
src/components/page-headers/PageHeader--Compact.astro | 50 | Minimal header |
Modified Files (1)
| File | Changes |
src/pages/changelog/index.astro | Now uses layouts, toggler, client-side switching |
Usage
Using the Layouts
---
import TimelineLayout from '@layouts/changelog/TimelineLayout.astro';
import { getCollection } from 'astro:content';
const entries = await getCollection('changelog');
---
<TimelineLayout entries={entries} groupedByMonth={groupedByMonth} />Using Page Headers
---
import PageHeaderCentered from '@components/page-headers/PageHeader--Centered.astro';
---
<PageHeaderCentered
title="Changelog"
subtitle="Track the evolution of Dark Matter."
badgeText="Development Timeline"
badgeIcon="clock"
>
<div slot="actions">
<!-- Optional action buttons -->
</div>
</PageHeaderCentered>Why This Architecture?
Single Page, Multiple Views: Users can toggle views instantly without losing scroll position or reloading
SSR Compatible: CSS data attributes ensure correct first paint before JS hydrates
Shareable URLs: View preference persists via URL parameter
DRY Components: Button and header components reusable across the site
Separation of Concerns: Layouts handle data display, togglers handle interaction
Summary
This refactor transforms the changelog from three separate pages into a unified, client-interactive experience. The pattern of "render all views, toggle visibility" trades slightly larger initial HTML for instant interactivity and better UX.
Code reduction: ~400 lines of duplicated code eliminated Components created: 8 reusable components User experience: Instant view switching, shareable URLs, consistent styling