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 releases
Each 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=parameter - Toast 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