# Blog Table of Contents Component Documentation

**Last Updated:** 2026-01-18

Complete documentation for the Blog Table of Contents (INHALT) component, including API, usage, configuration, and customization options.

## Overview

The Blog TOC component automatically generates a table of contents for blog posts by extracting headings from post content. It provides:

- **Automatic heading extraction** from HTML content
- **Dynamic ID generation** for headings without IDs
- **Scroll spy functionality** to highlight the active section
- **Smooth scrolling** to sections when clicked
- **Responsive design** (hidden on mobile, sidebar on desktop)
- **Accessibility features** (ARIA labels, keyboard navigation)

## Component Files

- **PHP Component**: `v2/components/blog/BlogTOC.php`
- **JavaScript Module**: `v2/js/blog-toc.js`
- **CSS Styles**: `v2/css/blog-post.css` (`.blog-toc` classes)
- **Helper Functions**: `v2/config/blog-template-helpers.php`

## Usage

### Basic Usage

The TOC is automatically included in blog post templates (`v2/pages/blog/post.php`). No manual inclusion needed.

### Manual Inclusion (if needed)

```php
<?php
// Extract headings and build TOC
$headings_data = extract_headings_from_html($html_content);
$html_with_ids = add_ids_to_headings($html_content, $headings_data);
$headings_updated = extract_headings_from_html($html_with_ids);
$toc_items = build_toc_structure($headings_updated);

// Include TOC component
if (!empty($toc_items)) {
    include __DIR__ . '/../../components/blog/BlogTOC.php';
}
?>
```

## Component API

### BlogTOC.php Component

**Props:**

- `$toc_items` (array, required) - Array of TOC items:
  ```php
  [
      [
          'id' => 'heading-id',
          'label' => 'Heading Text',
          'level' => 2  // H2, H3, or H4
      ],
      // ...
  ]
  ```
- `$options` (array, optional) - Configuration options:
  - `min_headings` (int, default: 2) - Minimum headings to show TOC

**Example:**

```php
$toc_items = [
    ['id' => 'introduction', 'label' => 'Introduction', 'level' => 2],
    ['id' => 'step-1', 'label' => 'Step 1: Setup', 'level' => 2],
    ['id' => 'step-1-details', 'label' => 'Details', 'level' => 3],
];

$options = ['min_headings' => 2];

include __DIR__ . '/../../components/blog/BlogTOC.php';
```

## Helper Functions

### `generate_heading_id($text, &$existing_ids = [])`

Generates a URL-safe ID from heading text.

**Parameters:**

- `$text` (string) - Heading text
- `$existing_ids` (array, by reference) - Array of existing IDs to ensure uniqueness

**Returns:** (string) URL-safe ID

**Example:**

```php
$existing_ids = [];
$id1 = generate_heading_id('Was ist ein Urlaubsantrag?', $existing_ids);
// Returns: 'was-ist-ein-urlaubsantrag'

$id2 = generate_heading_id('Was ist ein Urlaubsantrag?', $existing_ids);
// Returns: 'was-ist-ein-urlaubsantrag-2' (duplicate handled)
```

### `extract_headings_from_html($html_content)`

Extracts all headings (H2-H6) from HTML content.

**Parameters:**

- `$html_content` (string) - HTML content

**Returns:** (array) Array of heading data:
```php
[
    [
        'level' => 2,
        'text' => 'Heading Text',
        'id' => 'existing-id' | null,
        'full_match' => '<h2>Heading Text</h2>',
        'attrs' => 'class="..."'
    ],
    // ...
]
```

### `add_ids_to_headings($html_content, $headings_data)`

Adds ID attributes to headings that don't have them.

**Parameters:**

- `$html_content` (string) - HTML content
- `$headings_data` (array) - Array from `extract_headings_from_html()`

**Returns:** (string) HTML content with IDs added

### `build_toc_structure($headings_data, $options = [])`

Builds TOC data structure from headings. Automatically groups H3s under H2s when TOC exceeds threshold for better UX.

**Parameters:**

- `$headings_data` (array) - Array from `extract_headings_from_html()`
- `$options` (array, optional) - Configuration:
  - `max_depth` (int, default: 3) - Maximum heading level (2=H2 only, 3=H2+H3, 4=H2+H3+H4)
  - `min_headings` (int, default: 2) - Minimum headings to return TOC
  - `group_h3s` (bool|null, default: null) - Group H3s under parent H2s. `null` = auto-detect based on count
  - `max_items_before_grouping` (int, default: 15) - Threshold for auto-grouping. If total items > this, H3s are grouped

**Returns:** (array) TOC items array. Structure depends on grouping:
- **Flat structure** (≤15 items): `[['id' => string, 'label' => string, 'level' => int], ...]`
- **Grouped structure** (>15 items): `[['id' => string, 'label' => string, 'level' => 2, 'children' => [['id' => string, 'label' => string, 'level' => 3], ...]], ...]`

**Grouping Behavior:**
- If total TOC items ≤15: Returns flat structure (all items at same level)
- If total TOC items >15: Returns grouped structure (H3s nested under parent H2s)
- H2s are always visible, H3s are collapsed by default in grouped mode

## JavaScript API

### Alpine.js Component: `blogTOC(sectionsData)`

**Parameters:**

- `sectionsData` (array) - Array of TOC sections (passed from PHP). Can be flat or grouped structure.

**Properties:**

- `activeSection` (string|null) - Currently active section ID
- `showTOC` (boolean) - Whether TOC is visible (based on scroll position AND article visibility)
- `sections` (array) - TOC sections data (grouped or flat)
- `flatSections` (array) - Flattened sections for active section detection
- `isGrouped` (boolean) - Whether TOC uses grouped structure
- `expandedSections` (object) - Object tracking which H2 sections are expanded (grouped mode only)
- `scrollThreshold` (number) - Scroll position threshold (default: 400px)
- `contentObserver` (IntersectionObserver|null) - Observer for blog post article element
- `isContentVisible` (boolean) - Whether article content is visible in viewport

**Methods:**

- `scrollToSection(id)` - Smooth scroll to section (auto-expands parent H2 if H3)
- `updateActiveSection()` - Update active section based on scroll
- `updateShowTOC()` - Update TOC visibility (checks scroll threshold AND article visibility)
- `setupContentObserver()` - Setup Intersection Observer for article element visibility
- `expandSection(h2Id)` - Expand H2 section to show H3 children (grouped mode)
- `collapseSection(h2Id)` - Collapse H2 section to hide H3 children (grouped mode)
- `toggleSection(h2Id)` - Toggle expand/collapse state of H2 section (grouped mode)
- `expandAll()` - Expand all H2 sections (grouped mode)
- `collapseAll()` - Collapse all H2 sections (grouped mode)
- `isExpanded(h2Id)` - Check if H2 section is expanded (grouped mode)
- `expandParentIfH3(sectionId)` - Auto-expand parent H2 when H3 becomes active (grouped mode)

## Configuration Options

### Minimum Headings

Control when TOC appears:

```php
$toc_items = build_toc_structure($headings_data, [
    'min_headings' => 3  // Show TOC only if 3+ headings
]);
```

### Maximum Depth

Control which heading levels appear in TOC:

```php
// H2 only
$toc_items = build_toc_structure($headings_data, ['max_depth' => 2]);

// H2 and H3 (default)
$toc_items = build_toc_structure($headings_data, ['max_depth' => 3]);

// H2, H3, and H4
$toc_items = build_toc_structure($headings_data, ['max_depth' => 4]);
```

### Collapsible Grouping

Control when H3s are grouped under H2s (improves UX for long TOCs):

```php
// Auto-detect grouping (default: groups if >15 items)
$toc_items = build_toc_structure($headings_data, [
    'max_depth' => 3,
    'max_items_before_grouping' => 15
]);

// Force grouping
$toc_items = build_toc_structure($headings_data, [
    'group_h3s' => true
]);

// Force flat structure (no grouping)
$toc_items = build_toc_structure($headings_data, [
    'group_h3s' => false
]);
```

**When Grouping is Applied:**
- Automatically enabled when TOC has >15 items
- H2 sections always visible
- H3 sections collapsed by default (expand on click)
- "Expand all" / "Collapse all" toggle available
- Parent H2 auto-expands when scrolling to H3 section

### Scroll Threshold

Adjust when TOC appears/disappears:

```javascript
// In blog-toc.js, modify scrollThreshold
scrollThreshold: 800  // Show TOC after scrolling 800px
```

### Content-Based Hiding

The TOC automatically hides when users scroll past the blog post content into related sections (carousel, FAQ, footer). This provides a cleaner reading experience by removing navigation when it's no longer relevant.

**Behavior:**
- TOC appears after scrolling past 400px threshold (default)
- TOC disappears when blog post article (`#blog-post-content`) exits viewport
- TOC remains hidden for related posts carousel, FAQ section, and footer
- TOC reappears when scrolling back up to article content

**Implementation:**
- Uses Intersection Observer API to detect when article element exits viewport
- More performant than scroll-based calculations
- Gracefully falls back to scroll-only behavior if article element not found

**Properties:**
- `isContentVisible` (boolean) - Tracks if article content is visible in viewport
- `contentObserver` (IntersectionObserver|null) - Observer instance for article element

**Methods:**
- `setupContentObserver()` - Initializes Intersection Observer for article element
- `updateShowTOC()` - Updates TOC visibility based on scroll threshold AND article visibility

**Technical Details:**
- Article element must have `id="blog-post-content"` (automatically added in `post.php`)
- Observer uses `threshold: 0` to trigger when any part of article exits viewport
- TOC visibility logic: `showTOC = pastScrollThreshold && isContentVisible`

## Styling Customization

### CSS Classes

**Base Classes:**
- `.blog-toc` - Main container (fixed positioning)
- `.blog-toc-nav` - Navigation element (240px fixed width, includes padding/border)
- `.blog-toc-item` - Individual TOC item (text wrapping enabled)
- `.blog-toc-item--active` - Active TOC item
- `.blog-toc-item--h2` - H2 level item
- `.blog-toc-item--h3` - H3 level item (indented in grouped mode)
- `.blog-toc-item--h4` - H4 level item

**Grouped TOC Classes:**
- `.blog-toc-group` - Container for H2 with its H3 children
- `.blog-toc-toggle` - Expand/collapse button for H2 sections
- `.blog-toc-chevron` - Chevron icon (rotates when expanded)
- `.blog-toc-children` - Container for H3 items under H2 (indented, collapsible)

### Width Constraints

**Fixed Width: 240px**

The TOC has a **fixed width of 240px** to ensure consistent layout across all blog posts. This width is enforced through:

1. **Tailwind class**: `w-[240px]` in `BlogTOC.php`
2. **CSS rules**: Explicit width constraints in `blog-post.css`

**Critical CSS Rules** (do not modify without testing):

```css
.blog-toc-nav {
  width: 240px !important;
  max-width: 240px !important;
  min-width: 240px !important;
  box-sizing: border-box;
}

.blog-toc-item {
  white-space: normal !important;
  word-break: break-word;
  overflow-wrap: break-word;
  min-width: 0;
  flex-shrink: 1;
}
```

**Text Wrapping**

Long TOC items automatically wrap within the 240px width:

- `word-break: break-word` - Break long words if needed
- `overflow-wrap: break-word` - Wrap text at word boundaries
- `white-space: normal` - Allow text wrapping
- `hyphens: auto` - Add hyphens for better readability

**Width Calculation**

The 240px width includes:
- Content width: ~214px
- Padding: 12px each side (p-3 = 0.75rem)
- Border: 1px each side
- **Total**: 240px

### Custom Styles

Add custom styles to `v2/css/blog-post.css`:

```css
.blog-toc-item--active {
    /* Custom active state */
    background-color: #your-color;
}

.blog-toc-item:hover {
    /* Custom hover state */
    background-color: #your-hover-color;
}
```

## Accessibility

### ARIA Labels

- `aria-label="Inhalt"` on nav element
- `aria-current="location"` on active TOC item
- `role="complementary"` on TOC container

### Keyboard Navigation

- Tab through TOC links
- Enter/Space to activate links
- Focus states visible with outline

### Screen Readers

- Semantic `<nav>` element
- Descriptive labels
- Active section announced

## Performance

### Optimizations

- **Intersection Observer API** for scroll spy (not scroll events)
- **Throttled scroll listener** for show/hide
- **Server-side rendering** of TOC HTML
- **Minimal JavaScript** footprint

### Browser Support

- Modern browsers (Chrome, Firefox, Safari, Edge)
- Intersection Observer fallback for older browsers
- Graceful degradation if JavaScript disabled

## Troubleshooting

### TOC Not Appearing

1. Check if post has enough headings (minimum 2 by default)
2. Verify headings are H2-H4 (configurable)
3. Check browser console for JavaScript errors
4. Verify `blog-toc.js` is loaded

### TOC Width Issues

**Problem**: TOC appears wider than 240px in production

**Solution**:
1. Verify minified CSS is up-to-date: Run `npm run minify`
2. Check computed CSS in DevTools: Should show `width: 240px`
3. Clear browser cache and hard refresh
4. Verify no conflicting CSS rules override width
5. Check that `blog-post.min.css` includes width constraints:
   ```bash
   grep "blog-toc-nav" v2/css/blog-post.min.css
   ```
   Should show: `width:240px!important;max-width:240px!important`

**Problem**: Text not wrapping in TOC items

**Solution**:
1. Verify `.blog-toc-item` has `white-space: normal !important`
2. Check for conflicting `white-space: nowrap` rules
3. Verify `word-break: break-word` is applied
4. Ensure `min-width: 0` on flex items to allow shrinking

### IDs Not Generated

1. Check `generate_heading_id()` function
2. Verify heading text is not empty
3. Check for special character handling issues

### Active Section Not Updating

1. Verify Intersection Observer is supported
2. Check browser console for errors
3. Verify heading IDs match TOC item IDs

## Testing

### Test Scripts

- `v2/scripts/blog/test-heading-extraction.php` - Test heading extraction
- `v2/scripts/blog/test-toc-generation.php` - Test TOC generation

### Manual Testing

1. Load a blog post with multiple headings
2. Scroll down - TOC should appear after ~600px
3. Click TOC items - should smooth scroll to sections
4. Scroll through content - active section should highlight
5. Test on mobile - TOC should be hidden
6. Test keyboard navigation - Tab through links

### TOC Width Testing Checklist

**Pre-Deployment Testing:**

- [ ] **Width Measurement**
  - [ ] Open DevTools → Inspect `.blog-toc-nav` element
  - [ ] Check computed CSS: `width` should be exactly `240px`
  - [ ] Check computed CSS: `max-width` should be `240px`
  - [ ] Check computed CSS: `min-width` should be `240px`
  - [ ] Check computed CSS: `box-sizing` should be `border-box`
  - [ ] Measure rendered width using DevTools ruler: should be `240px`

- [ ] **Text Wrapping**
  - [ ] Test with longest TOC item (e.g., "Wie berechnet man Arbeitsstunden pro Monat bei Teilzeit?")
  - [ ] Verify text wraps to multiple lines within 240px width
  - [ ] Check computed CSS: `white-space` should be `normal`
  - [ ] Check computed CSS: `word-break` should be `break-word`
  - [ ] Verify no horizontal overflow

- [ ] **Cross-Browser Testing**
  - [ ] Chrome/Edge: Verify width is 240px
  - [ ] Firefox: Verify width is 240px
  - [ ] Safari: Verify width is 240px
  - [ ] Check for browser-specific CSS issues

- [ ] **Responsive Testing**
  - [ ] Desktop (≥1024px): TOC visible, width 240px
  - [ ] Tablet (768-1023px): TOC hidden (lg:block)
  - [ ] Mobile (<768px): TOC hidden

- [ ] **Production vs Local**
  - [ ] Compare local width with production width
  - [ ] Verify minified CSS includes width constraints
  - [ ] Check for CSS cache issues
  - [ ] Verify no production-specific overrides

**Post-Deployment Verification:**

- [ ] Production width matches local (240px)
- [ ] No layout shifts or visual glitches
- [ ] Text wraps correctly for all TOC items
- [ ] No console errors related to TOC
- [ ] Performance: No layout thrashing

**Measurement Steps:**

1. Open browser DevTools (F12)
2. Select `.blog-toc-nav` element
3. In Computed tab, verify:
   - `width: 240px`
   - `max-width: 240px`
   - `box-sizing: border-box`
4. In Elements tab, verify:
   - No inline styles overriding width
   - Tailwind class `w-[240px]` is present
5. Use DevTools ruler to measure visual width: should be exactly 240px

## Related Documentation

- [Blog Template Development Guide](guides/TEMPLATE_DEVELOPMENT_GUIDE.md)
- [Blog TOC Migration Guide](../BLOG_TOC_MIGRATION.md)
- [Heading Hierarchy Guide](../HEADING_HIERARCHY_GUIDE.md)
