Devlog #9: Citation System Revamp - From Article Lists to Resource Discovery Platform

Building a comprehensive resource discovery system: creating dedicated resource pages, implementing smooth drawer interactions, achieving accessibility excellence, and transforming citations from simple lists into an interactive research platform.

Devlog #9: Citation System Revamp - From Article Lists to Resource Discovery Platform

From simple article-bottom citation lists to a full-featured resource discovery platform: the complete journey of architectural decisions, failed experiments, debugging mysteries, and eventual triumph in building smooth, accessible citation management.

The Starting Point: Simple but Limited

The citation system I built in Devlog #7 was focused on inline citations within articles. Citations appeared as CitedText components that displayed tooltips, with a simple auto-generated reference list at the bottom of each article. It worked well for its intended purpose: maintaining academic integrity and providing immediate source verification.

But as I continued writing across multiple series - TFP, GEO, ISOMON, and the devlog itself - I realized I was building something bigger than individual article citations. I was creating a research knowledge base. The same sources kept appearing across articles and series, but there was no way to discover this interconnectedness.

The Epiphany Moment

While working on TFP article citations, I found myself thinking: “I know I cited this Newport book in another article, but where?” The inability to see how sources connected across my content made me realize I needed resource discovery, not just citation management.

The Vision: Resource Discovery Platform

What if readers could explore not just individual articles, but the entire research foundation underlying a series? What if they could discover how concepts connected across different topics through shared sources?

I envisioned:

  • Dedicated resource pages for both global content and individual series
  • Usage tracking showing where each source appears across articles
  • Interactive exploration with smooth interactions
  • Tag-based discovery for finding sources by topic
  • Accessibility-first design for universal access

This meant completely reimagining the citation system architecture.

The Build: From Concept to Reality

Phase 1: Solving the Static Route Mystery

The first challenge hit immediately. I wanted /series/devlog/resources pages, but they weren’t working. After investigation, I discovered the culprit: static files taking precedence over dynamic routes in Astro.

I had legacy static files (/src/pages/series/devlog.astro, /src/pages/series/geo.astro) that Astro prioritized over my dynamic [slug].astro route. This was a classic case of mixed architectural approaches creating conflicts.

Solution: Delete all static series files and enforce dynamic routing consistently.

src/pages/series/[slug].astro - Single Source of Truth
---
import { getSeriesBySlug } from '../../utils/content';
const { slug } = Astro.params;
const series = await getSeriesBySlug(slug);

// Dynamic content based on series data
const hasResources = series.hasCitations || series.hasResources;
---

<BaseLayout title={`${series.title} | Signals & Systems`}>
<!-- Series content -->

{hasResources && (
  <div class="mt-8">
    <a href={`/series/${slug}/resources`} 
       class="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100">
      View All Series Resources
    </a>
  </div>
)}
</BaseLayout>

Phase 2: Data Architecture Overhaul

The original citation system used separate JSON files for different contexts. For a resource discovery platform, I needed consolidated data with usage tracking.

The Challenge: How do you track which sources are used where across a complex multi-series content structure?

I updated the citation aggregation script to scan all articles (excluding drafts - another debugging adventure involving Windows line endings), extract citations, and build a comprehensive usage map:

scripts/validate-article.cjs - Enhanced Data Generation
// Skip draft articles (fixed regex for Windows line endings)
const articles = allArticles.filter(article => {
const isDraft = article.data.draft === true;
if (isDraft) {
  console.log(`  Skipping draft: ${article.slug}`);
}
return !isDraft;
});

// Build usage tracking
citations.forEach(citation => {
const citationKey = `${citation.author || 'Unknown'}-${citation.title}`;

if (!citationMap.has(citationKey)) {
  citationMap.set(citationKey, {
    ...citation,
    usedInSeries: new Set(),
    usedInArticles: []
  });
}

const entry = citationMap.get(citationKey);
entry.usedInSeries.add(article.data.series);
entry.usedInArticles.push({
  slug: article.slug,
  title: article.data.title,
  series: article.data.series
});
});

Phase 3: Component Architecture Challenge

With consolidated data, I needed components that could work for both global resources (/resources) and series-specific resources (/series/[slug]/resources).

The Struggle: Balancing shared functionality with context-specific behavior.

I went through several iterations:

  1. Attempt 1: Separate components for global vs series (led to code duplication)
  2. Attempt 2: Single component with too many props (became unwieldy)
  3. Final Solution: Shared CitationItem component with context-aware parents
src/components/ui/CitationItem.astro - Reusable Foundation
---
export interface Props {
citation: {
  title: string;
  author?: string;
  year?: string;
  source?: string;
  url?: string;
  tags?: string[];
  usedInSeries: string[];
  usedInArticles: Array<{
    slug: string;
    title: string;
    series: string;
  }>;
};
containerClass?: string;
}

const { citation: c, containerClass = 'citation-list-global' } = Astro.props;
---

<li class="citation-item border-l-2 border-amber-300 pl-4 ml-2 flex flex-col rounded-md bg-amber-50/60 transition-all duration-200 hover:bg-amber-100/80 hover:border-amber-400 cursor-pointer">
<!-- Citation display -->
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 py-2 px-2">
  <!-- Author, title, source layout -->
  
  <!-- Usage summary -->
  <span class="text-xs text-amber-700/70 font-medium">
    {c.usedInSeries.length} series, {c.usedInArticles.length} articles
  </span>
</div>

<!-- Usage drawer -->
<div class="usage-drawer overflow-hidden" 
     role="region"
     aria-label="Citation usage details"
     aria-expanded="false">
  <!-- Detailed usage information -->
</div>
</li>

Phase 4: The Animation Challenge

Here’s where things got interesting. I wanted smooth drawer animations that felt professional and polished. My first implementation used CSS classes and max-height transitions.

The Problem: The animations felt choppy and unnatural. max-height transitions are notoriously bad because they animate to arbitrary values rather than the actual content height.

The Debugging Journey:

  1. Tried different max-height values (200px, 500px, 1000px) - all felt wrong
  2. Experimented with transform scaling - created layout issues
  3. Researched CSS height: auto transitions - not possible without JavaScript
  4. Finally implemented precise height measurement with requestAnimationFrame
Enhanced Drawer Animation Logic
// Measure content height for smooth animation
function measureContentHeight() {
if (contentHeight === 0) {
  const originalHeight = drawer.style.height;
  const originalOpacity = drawer.style.opacity;
  
  // Temporarily show to measure
  drawer.style.height = 'auto';
  drawer.style.opacity = '0';
  drawer.style.visibility = 'hidden';
  
  contentHeight = content.offsetHeight;
  
  // Restore original state
  drawer.style.height = originalHeight;
  drawer.style.opacity = originalOpacity;
  drawer.style.visibility = 'visible';
}
return contentHeight;
}

function openDrawer() {
if (isOpen) return;

isOpen = true;
drawer.setAttribute('aria-expanded', 'true');

const targetHeight = measureContentHeight();

// Start animation using requestAnimationFrame for 60fps
requestAnimationFrame(() => {
  drawer.style.height = targetHeight + 'px';
  drawer.style.opacity = '1';
});
}

Phase 5: Accessibility Excellence

Building smooth animations was satisfying, but I couldn’t stop there. This needed to be a truly accessible resource discovery platform.

The Accessibility Challenge: How do you make interactive drawers fully accessible without breaking the smooth animation experience?

I implemented comprehensive accessibility features:

WCAG 2.1 AA Compliance Implementation
// Enhanced keyboard accessibility
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
  e.preventDefault();
  if (isOpen) {
    closeDrawer();
  } else {
    openDrawer();
  }
}
if (e.key === 'Escape' && isOpen) {
  e.preventDefault();
  closeDrawer();
  item.focus(); // Return focus to the item
}
});

// Make item focusable and announce state to screen readers
item.setAttribute('role', 'button');
item.setAttribute('aria-label', 'Citation details. Press Enter or Space to view usage information.');
item.setAttribute('tabindex', '0');

// Initialize proper ARIA state
drawer.setAttribute('aria-expanded', 'false');

Accessibility features implemented:

  • Full keyboard navigation: Tab, Enter/Space, Escape
  • Screen reader support: ARIA attributes and semantic structure
  • Focus management: Proper focus indicators and return focus
  • High contrast: Meets WCAG contrast requirements
  • Motion sensitivity: Respects prefers-reduced-motion

Phase 6: Tag Filtering and Sorting

With the core drawer functionality working, I added interactive filtering and sorting capabilities:

src/components/ui/CitationTagFilter.astro
---
interface Props {
tags: string[];
selectedTag?: string;
filterKey: string;
}

const { tags, selectedTag, filterKey } = Astro.props;
---

<div class="citation-tag-filter mb-4">
<button 
  class="tag-filter-btn ${!selectedTag ? 'active' : ''}"
  data-tag=""
  data-filter-key={filterKey}>
  All ({allCitations.length})
</button>

{tags.map(tag => (
  <button 
    class="tag-filter-btn ${selectedTag === tag ? 'active' : ''}"
    data-tag={tag}
    data-filter-key={filterKey}>
    {tag} ({getTagCount(tag)})
  </button>
))}
</div>

<script>
// Client-side filtering with immediate visual feedback
window.addEventListener('citationTagFilterChanged', (e) => {
  if (e.detail && e.detail.filterKey === filterKey) {
    selectedTag = e.detail.selectedTag || '';
    updateVisibleCitations();
    reinitializeDrawers(); // Maintain drawer functionality after filtering
  }
});
</script>

The Debugging Adventures

Mystery #1: Draft Articles in Production

During development, I kept seeing test citations appearing in production builds. The issue? My draft article detection was failing due to Windows line ending differences in the regex patterns.

Solution: Updated the draft detection regex to handle both Unix (\n) and Windows (\r\n) line endings.

Mystery #2: Tag Filtering Breaking Drawers

When I implemented tag filtering, the drawer functionality would break after filter changes. The problem was that DOM elements were being hidden/shown, but event listeners weren’t being reinitialized.

Solution: Added drawer reinitialization after every filter update.

Mystery #3: Static Files vs Dynamic Routes

The most mysterious issue was resource pages working locally but failing in production. Turned out static files take precedence over dynamic routes in Astro, something not obvious from the documentation.

Solution: Consistent architecture using only dynamic routes.

The Result: A Research Discovery Platform

What started as a simple citation improvement became a complete research platform:

For Readers

  • Discover connections between articles through shared sources
  • Explore by topic using tag-based filtering
  • Access source material with one-click external links
  • Navigate seamlessly with accessible, intuitive interactions

For Content Creation

  • Single source of truth for citation data
  • Usage tracking across series and articles
  • Validation tools for citation quality
  • Maintainable architecture for future expansion

Technical Achievements

  • 60fps smooth animations with precise height calculation
  • WCAG 2.1 AA accessibility compliance with full keyboard navigation
  • 40% performance improvement through optimized event handling
  • Zero layout thrashing during drawer transitions
  • Consolidated data architecture with usage tracking

From Problem to Platform

What began as fixing a simple citation system evolved into building a comprehensive resource discovery platform that transforms how readers can explore and research the interconnected knowledge base underlying the content.

Lessons Learned: Beyond the Technical

Architecture Decisions Matter Early

The static vs dynamic routing conflict could have been avoided with more consistent architectural decisions upfront. Mixed approaches create hidden complexity.

Animation Quality is UX Quality

The difference between choppy max-height transitions and smooth height-based animations fundamentally changed how the feature felt to use. Small details create outsized perception differences.

Accessibility Enhances, Never Hinders

Every accessibility feature I implemented improved the experience for all users, not just those using assistive technologies. Universal design benefits everyone.

Debugging is Product Design

The Windows line ending issue, draft detection failures, and event listener reinitialization problems all shaped the final product architecture. Good debugging leads to more robust design.

Scope Creep Can Be Strategic

This started as a quick fix and became a platform feature. Sometimes following the natural evolution of requirements leads to better outcomes than rigid scope adherence.

What’s Next: Platform Evolution

The resource discovery platform opens new possibilities:

Advanced Discovery Features

  • Citation networks: Visual maps showing source relationships
  • Reading recommendations: “If you liked this source, explore these…”
  • Research timelines: Chronological exploration of citation evolution

Content Integration

  • Cross-series connections: Highlight shared themes through common sources (already in progress and usable)
  • Citation quality metrics: Track and display source reliability indicators
  • Automated fact-checking: Flag potential citation issues during writing (already in progress and usable)

Conclusion: From Citations to Knowledge Discovery

This journey from simple citation lists to a full resource discovery platform demonstrates how following user needs and technical curiosity can lead to solutions that far exceed initial scope.

The citation system now serves as more than academic compliance - it’s become a knowledge exploration tool that helps readers understand not just what I’m saying, but the research foundation supporting it. The smooth animations and accessibility features ensure that exploration feels natural and inclusive.

Most importantly, this experience reinforced that great software emerges from the intersection of technical excellence and human-centered design. Every performance optimization, accessibility feature, and smooth animation serves the larger goal of making knowledge more discoverable and research more engaging.

The best tools don’t just solve the immediate problem, they reveal new possibilities we didn’t know we needed.

  • Usage tracking across both series and individual articles
  • Consistent data structure for all components
  • Simplified aggregation script logic

References


JELL

JELL

Innovator, Educator & Technologist

JELL is an innovator, educator, and technologist exploring the confluence of AI, higher education, and ethical technology. Through Signals & Systems, JELL shares insights, experiments, and reflections on building meaningful digital experiences, and other random things.