Reference Grid Rendering and Table of Contents Improvements

Summary

Fixed reference grid rendering issues, improved table of contents functionality, and enhanced layout structure across multiple components.

Why Care

These changes slightly improve the user experience by fixing critical rendering issues in the reference grid and providing a more robust table of contents navigation system. The unified slug generation approach ensures consistent behavior across the application.

Implementation

Changes Made

  • Fixed issue where originalFilename was undefined for nested entries in the reference grid
  • Created referenceSlugMap.ts as a single source of truth for slug/filename mapping
  • Implemented recursive ToC rendering with proper depth-based styling
  • Enhanced title generation logic for Essays, Concepts, and Vocabulary collections to preserve original casing
  • Fixed TypeScript errors in collection handling
  • Moved landing page layout to a simplified layout structure

Key Files Modified:

  • /site/src/utils/referenceSlugMap.ts (new file)
  • /site/src/pages/more-about/index.astro
  • /site/src/pages/more-about/[...slug].astro
  • /site/src/components/markdown/TableOfContents.astro
  • /site/src/layouts/SingleColumnSectionScroll.astro (new file)
  • /site/src/pages/read/index.astro
  • /site/src/components/articles/ArticleListNewsPreview.astro

Technical Details

Reference Grid Rendering Fix

The issue with originalFilename being undefined for nested entries was resolved by creating a centralized slug generation approach in referenceSlugMap.ts and ensuring both the map creation and lookup use the same logic:
typescript
// site/src/utils/referenceSlugMap.ts
// CRITICAL FIX: Use the SAME slug generation logic that's used in index.astro
// This ensures the key used for lookup matches the key created here
// We use the normalized entry.id as the key, which matches how index.astro generates slugs
const slug = getReferenceSlug(entry.id, entry.data.slug);
map[slug] = filenameNoExt;
The critical getReferenceSlug function provides a consistent way to generate slugs throughout the application:
typescript
// site/src/utils/referenceSlugMap.ts
export function getReferenceSlug(filename: string, frontmatterSlug?: string): string {
  // Match getStaticPaths: use frontmatter slug if present, else slugify the filename
  return frontmatterSlug || slugify(filename);
}

export function getOriginalFilenameMap(
  entries: { id: string; data: { slug?: string } }[],
  collectionDir: string
): Record<string, string> {
  // DEBUG: Print collectionDir and files
  console.log('[getOriginalFilenameMap] collectionDir:', collectionDir);
  const files = getAllMarkdownFiles(collectionDir, collectionDir);
  console.log('[getOriginalFilenameMap] files:', files);

  // Aggressively comment: This function guarantees slug -> true original filename (with correct casing)
  // by reading ALL .md filenames directly from the filesystem, recursively.
  // This supports concepts in nested folders (e.g., CARBS/Explainers for AI/ etc).
  // Do not trust Astro's id or slug for casing!
  const map: Record<string, string> = {};
  
  // DEBUG: Enhanced logging
  console.log(`[getOriginalFilenameMap] Processing ${entries.length} entries against ${files.length} files`);
  
  for (const entry of entries) {
    // DEBUG: Print entry.id and entry.data.slug
    console.log('\n[getOriginalFilenameMap] Processing entry.id:', entry.id);
    console.log('[getOriginalFilenameMap] entry.data.slug:', entry.data.slug);

    // Astro's entry.id is relative to the content root (e.g., concepts/Explainers for AI/AI Hallucinations.md)
    // Remove the collectionDir prefix, then remove .md and lowercase for matching.
    // Robust normalization: lowercase, replace spaces/dashes with dashes, strip .md
    // This allows matching between Astro's slugified id and the original filesystem path
    const normalize = (s: string) => {
      const result = s
        .replace(/\.md$/, '')
        .toLowerCase()
        .replace(/[\s_]+/g, '-')
        .replace(/-+/g, '-');
      return result;
    };
    
    const entryNorm = normalize(entry.id);
    // DEBUG: Print entryNorm
    console.log('[getOriginalFilenameMap] entryNorm:', entryNorm);
    
    // DEBUG: Show all normalized files for comparison
    console.log('[getOriginalFilenameMap] Looking for match among normalized files:');
    const normalizedFiles = files.map(f => ({ original: f, normalized: normalize(f) }));
    normalizedFiles.forEach(nf => {
      const isMatch = nf.normalized === entryNorm;
      console.log(`  ${isMatch ? '✓' : '✗'} ${nf.normalized} (from ${nf.original})`);
    });
    
    // Find the real file from the filesystem list by matching normalized path
    const realFile = files.find(f => normalize(f) === entryNorm);
    // DEBUG: Print realFile
    console.log('[getOriginalFilenameMap] realFile found:', realFile);
    
    if (realFile) {
      // Extract only the filename (no directory, no extension, original casing) for the map value
      // Example: 'Explainers for AI/World Foundation Models.md' -> 'World Foundation Models'
      const filenameNoExt = path.basename(realFile, '.md');
      
      // CRITICAL FIX: Use the SAME slug generation logic that's used in index.astro
      // This ensures the key used for lookup matches the key created here
      // We use the normalized entry.id as the key, which matches how index.astro generates slugs
      const slug = getReferenceSlug(entry.id, entry.data.slug);
      console.log(`[getOriginalFilenameMap] Using entry.id for slug key: ${slug}`);
      map[slug] = filenameNoExt;
      // DEBUG: Print the final mapping for this slug
      console.log(`[getOriginalFilenameMap] ADDED TO MAP: map[${slug}] = ${filenameNoExt}`);
    } else {
      console.log(`[getOriginalFilenameMap] ⚠️ NO MATCH FOUND for entry.id: ${entry.id}`);
      console.log(`[getOriginalFilenameMap] This entry will have undefined originalFilename`);
    }
  }
  
  // DEBUG: Show final map contents
  console.log('\n[getOriginalFilenameMap] Final map contents:');
  Object.entries(map).forEach(([k, v]) => {
    console.log(`  map[${k}] = ${v}`);
  });
  
  return map;
}
In site/src/pages/more-about/index.astro, we updated the directory paths and ensured consistent slug generation:
typescript
// Always use the absolute path to the true content directories (not site/src/content!)
// This guarantees correct casing on all platforms
const vocabularyDir = path.join(process.cwd(), 'src/generated-content/vocabulary');
const conceptsDir = path.join(process.cwd(), 'src/generated-content/concepts');

const vocabularySlugToOriginal = getOriginalFilenameMap(processedVocabularyEntries, vocabularyDir);
const conceptSlugToOriginal = getOriginalFilenameMap(processedConceptsEntries, conceptsDir);

// Correct assignment: use full relative path (minus .md) for slug generation and lookup
const vocabularyItems: ReferenceItem[] = processedVocabularyEntries.map(entry => {
  const filename = entry.id.replace(/\.md$/, ''); // full relative path, no extension
  const slug = getReferenceSlug(filename, entry.data.slug);
  // ...
});
This ensures consistent key mapping between getOriginalFilenameMap and index.astro, fixing the issue where nested entries had undefined originalFilename values.

Table of Contents Improvements

Implemented a recursive ToC rendering approach that properly handles nested headings up to 4 levels deep:
astro
<!-- site/src/components/markdown/TableOfContents.astro -->
{tocMap && (
  <ul>
    {tocMap.children.map((itemNode, idx) => {
      // First level rendering logic
      return (
        <li class={`toc-depth-1`}>
          {link && <a href={link.url}>{link.children && link.children[0]?.value}</a>}
          {nestedList && (
            <ul>
              {/* Second level rendering logic */}
            </ul>
          )}
        </li>
      );
    })}
  </ul>
)}
Added depth-based styling for ToC items to improve visual hierarchy:
css
.toc-depth-1 {
  font-size: 1.05em;
  font-weight: 500;
  margin-left: 0;
}
.toc-depth-2 {
  font-size: 1em;
  font-weight: 400;
  margin-left: 1.25em;
}

Title Generation Logic

Enhanced the title generation logic in ArticleListNewsPreview.astro to preserve original casing:
typescript
// Use the filename as the fallback title, preserving original casing
// - Replace dashes/underscores with spaces
// - Collapse multiple spaces
// - DO NOT change the case of any letters (e.g., 'API' stays 'API')
title = filename
  .replace(/[-_]/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

TypeScript Fixes

Fixed TypeScript errors in read/index.astro by adding proper type assertions:
typescript
// Type assertion to ensure TypeScript recognizes these as strings
const titleA = (a.data as EssayFrontmatter).title || '';
const titleB = (b.data as EssayFrontmatter).title || '';
return titleA.localeCompare(titleB);

Integration Points

  • The new referenceSlugMap.ts utility is now the central source of truth for all slug generation and filename mapping
  • The enhanced Table of Contents component is used across all content pages that display headings
  • The improved title generation logic affects how titles are displayed in article lists and reference grids
  • Directory paths for vocabulary and concepts now point to src/generated-content instead of ../content

Documentation

  • The changes maintain backward compatibility with existing content
  • No API changes were introduced
  • The slug generation logic now consistently uses the same approach across all components
  • The Table of Contents component now properly handles nested headings and provides better visual hierarchy