# blog-templates-components Full Instructions

## Component Usage Patterns

### PostCard Component

**Props Required**:

- `$title`, `$url`, `$category`, `$category_label`, `$excerpt`, `$publication_date`
- Optional: `$featured_image`, `$show_excerpt`, `$variant`

**Usage**:

```php
$title = $post['title'];
$url = $post['url'];
$category = $post['category'];
$category_label = get_category_label($category);
$excerpt = $post['excerpt'];
$publication_date = $post['publication_date'];
$featured_image = $post['featured_image'];
include '../../components/blog/PostCard.php';
```

**Variants**:

- `'default'` - Full card with excerpt (index/category pages)
- `'compact'` - Smaller card without excerpt (related posts)

### PostHeader Component

**Props Required**:

- `$title`, `$category`, `$category_label`, `$publication_date`
- Optional: `$author`, `$reading_time`, `$featured_image`

**Usage**:

```php
$title = $post['title'];
$category = $post['category'];
$category_label = get_category_label($category);
$publication_date = $post['publication_date'];
$author = $post['author'];
$reading_time = calculate_reading_time($post['content']['word_count']);
$featured_image = $post['featured_image'];
include '../../components/blog/PostHeader.php';
```

### PostContent Component

**Props Required**:

- `$html_content` - Pre-processed HTML content (images wrapped, tables wrapped, sanitized)
- Optional: `$images`, `$internal_links`, `$featured_image` (for reference only, not used in rendering)

**Usage**:

```php
$html_content = $post['content']['html'];
$images = $post['images'];
$internal_links = $post['internal_links'];
include '../../components/blog/PostContent.php';
```

**Features**:

- **Zero processing** - Content is pre-processed at extraction time
- **34 lines total** - Minimal component implementation
- **Images pre-wrapped** - Images are wrapped in lightbox containers during extraction
- **Tables pre-wrapped** - Tables are wrapped in breakout containers during extraction
- **Pre-sanitized** - HTML is sanitized during extraction (XSS prevention)
- **Ready to render** - Component simply outputs pre-processed HTML
- **Performance:** 0.01-0.03ms render time (99% reduction from baseline)

**Performance**: Render time is ~1-5ms (down from ~50-85ms) as all processing happens at extraction time.

### BlogTOC Component

**CRITICAL: Width Constraints**

The TOC has a **fixed width of 240px** that MUST be maintained:

- **HTML**: Tailwind class `w-[240px]` in `BlogTOC.php`
- **CSS**: Explicit width constraints in `blog-post.css`:
  ```css
  .blog-toc-nav {
    width: 240px !important;
    max-width: 240px !important;
    min-width: 240px !important;
    box-sizing: border-box;
  }
  ```
- **Text Wrapping**: Long TOC items MUST wrap within 240px:
  ```css
  .blog-toc-item {
    white-space: normal !important;
    word-break: break-word;
    overflow-wrap: break-word;
    min-width: 0;
    flex-shrink: 1;
  }
  ```
- **NEVER** modify width without:
  1. Testing in both local and production
  2. Regenerating minified CSS (`npm run minify`)
  3. Verifying text wrapping still works
  4. Updating documentation
  5. Running test script: `python3 v2/scripts/blog/test-toc-width.py`

**Props Required**:

- `$toc_items` (array) - Array of TOC items: `[['id' => string, 'label' => string, 'level' => int], ...]`
- Optional: `$options` (array) - Configuration: `['min_headings' => int]` (default: 2)

**Usage**:

```php
// Extract headings and build TOC
$html_content = $post['content']['html'] ?? '';
$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, [
    'max_depth' => 3,  // H2 and H3 only
    'min_headings' => 2
]);

// Include TOC component (automatically included in post.php template)
if (!empty($toc_items)) {
    include '../../components/blog/BlogTOC.php';
}
```

**Features**:

- **Automatic heading extraction** - Extracts H2-H6 from HTML content
- **Dynamic ID generation** - Generates URL-safe IDs for headings without IDs
- **Scroll spy** - Highlights active section using Intersection Observer API
- **Smooth scrolling** - Smooth scroll to sections when clicked
- **Responsive** - Hidden on mobile, sidebar on desktop (lg:block)
- **Accessibility** - ARIA labels, keyboard navigation, focus states

**Helper Functions**:

- `generate_heading_id($text, &$existing_ids)` - Generate URL-safe ID from text
- `extract_headings_from_html($html_content)` - Extract headings from HTML
- `add_ids_to_headings($html_content, $headings_data)` - Add IDs to headings
- `build_toc_structure($headings_data, $options)` - Build TOC data structure

**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 show TOC
- `max_items_before_grouping` (int, default: 15) - Threshold for auto-grouping H3s under H2s
- `group_h3s` (bool|null, default: null) - Force grouping. `null` = auto-detect based on count

**TOC Length Best Practices**:

- **≤15 items:** Flat structure (all items visible)
- **>15 items:** Grouped structure (H3s collapsed under H2s)
- **>30 items:** Consider content restructuring or H2-only mode

**Heading Structure Guidelines**:

- **H2:** Major sections (5-10 per post ideal)
- **H3:** Subsections under H2 (2-3 per H2 ideal, max 5)
- **Avoid:** Excessive H3s (>5 per H2) - split H2 or restructure content
- **Never:** Skip heading levels (H2 → H4)

**When to Use**:

- Automatically included in `post.php` template
- Only shows if post has 2+ headings (configurable)
- Only includes H2 and H3 by default (configurable)
- Auto-groups H3s when TOC exceeds 15 items (improves UX for long posts)

**Related Documentation**:

- `docs/content/blog/components/BLOG_TOC_COMPONENT.md` - Complete component documentation
- `docs/content/blog/BLOG_TOC_MIGRATION.md` - Migration guide

### RelatedPosts Component

**Props Required**:

- `$related_posts` (array of post data)
- Optional: `$limit` (default: 14), `$heading` (default: "Ähnliche Artikel")

**Usage**:

```php
$related_posts = load_related_posts($slug, $category, 14);
$heading = 'Ähnliche Artikel';
include '../../components/blog/RelatedPosts.php';
```

**Note**: Uses PostCard component with `$variant = 'compact'` and `$show_excerpt = false`

### Pagination Component

**Props Required**:

- `$current_page`, `$total_pages`, `$base_url`
- Optional: `$posts_per_page` (default: **18** in `Pagination.php` if unset; production templates should set `$posts_per_page = ordio_blog_listing_posts_per_page()` from `blog-template-helpers.php`), `$show_page_numbers` (default: true)

**Usage**:

```php
// Parse current page from URL (already done in template)
// $current_page, $total_pages, and $posts_per_page are already set

// For index pages:
$base_url = '/insights/';
$show_page_numbers = true;
include '../../components/blog/Pagination.php';

// For category pages:
$base_url = "/insights/{$page_category}/"; // Use $page_category, not $category
$show_page_numbers = true;
include '../../components/blog/Pagination.php';
```

**Important**:

- Don't create intermediate variables that overwrite `$current_page`
- For category pages, use `$page_category` for base_url to avoid conflicts with post category variable
- The Pagination component handles validation internally, so pass variables directly

**URL Generation**:

- Page 1: `/insights/` (no `/page/1/`)
- Page 2+: `/insights/page/2/`
- Category page 1: `/insights/{category}/` (no `/page/1/`)
- Category page 2+: `/insights/{category}/page/2/`

### CategoryNav Component

**Props Required**:

- `$current_category` (empty string for 'all')
- `$categories` (array from `load_blog_categories()`)
- Optional: `$variant` ('tabs' | 'links', default: 'tabs')

**Usage**:

```php
$current_category = $category;
$categories = load_blog_categories();
include '../../components/blog/CategoryNav.php';
```

### Breadcrumbs Component

**Props Required**:

- `$items` (array of ['name', 'url'] pairs)
- Optional: `$include_schema` (default: true)

**Schema Output Strategy**:

- **Blog pages**: Set `$include_schema = false` because JSON-LD BreadcrumbList is already provided via `render_blog_schema()` in `<head>`
- **Other pages**: Can use `$include_schema = true` (default) if Microdata breadcrumb schema is needed
- **Best practice**: Use JSON-LD only (preferred by Google) - disable Microdata when JSON-LD is present

**Usage for Blog Pages** (JSON-LD already provided):

```php
$items = [
    ['name' => 'Home', 'url' => '/'],
    ['name' => 'Blog', 'url' => '/insights/'],
    ['name' => $post['title'], 'url' => $post['url']]
];
$include_schema = false; // Disable Microdata - JSON-LD already in <head> via render_blog_schema()
include '../../components/blog/Breadcrumbs.php';
```

**Usage for Other Pages** (if Microdata needed):

```php
$items = [
    ['name' => 'Home', 'url' => '/'],
    ['name' => 'Page Name', 'url' => '/page/']
];
// $include_schema defaults to true if not set
include '../../components/blog/Breadcrumbs.php';
```

### TopicHubHero Component

**Props Required**:

- `$topic_name`
- Optional: `$topic_id`, `$description`, `$statistics`

**Usage**:

```php
$topic_name = $topic_data['name'];
$description = $topic_data['description'];
$statistics = [
    'post_count' => count($related_posts),
    'last_updated' => null,
    'subtopics_count' => 0
];
include '../../components/blog/TopicHubHero.php';
```

### Pillar Visual Components

**Purpose**: Display clean, card-based widget visuals for pillar pages in side-by-side layouts on blog index page.

**Components**:

1. **PillarVisualDienstplan** (`v2/components/blog/PillarVisualDienstplan.php`)

   - Clean card widget showing shift planning interface
   - Uses card-based design: rounded corners, border, shadow
   - White header with title "Dienstplanung"
   - SVG visualization of shift schedule

2. **PillarVisualZeiterfassung** (`v2/components/blog/PillarVisualZeiterfassung.php`)
   - Clean card widget showing time tracking interface
   - Simplified design matching Dienstplan visual style
   - White header with title "Zeiterfassung"
   - Digital time display (e.g., "08:45") with status indicator
   - No mobile phone mockup - uses card-based widget design

**Design Principles**:

- **Consistency**: Both visuals use same card structure and styling
- **Simplicity**: Focus on core visualization, remove unnecessary elements
- **Card-Based**: Use widget card design instead of complex mockups
- **Clean Typography**: Match Dienstplan visual typography patterns
- **Responsive Scaling**: Scale down to fit container (0.85 mobile, 0.9 desktop)

**CSS Classes**:

- `.pillar-visual-dienstplan` - Container for Dienstplan visual
- `.pillar-visual-zeiterfassung` - Container for Zeiterfassung visual
- `.pillar-visual-time-display` - Time display container
- `.pillar-visual-time-text` - Large digital time text
- `.pillar-visual-time-label` - Time label text
- `.pillar-visual-status-indicator` - Status indicator container
- `.pillar-visual-status-dot` - Animated status dot
- `.pillar-visual-status-text` - Status text

**Styling Patterns**:

- Card background: `bg-[#fbfbfb]`
- Border: `border border-[#EDEFF3]`
- Shadow: `shadow-lg`
- Rounded corners: `rounded-2xl`
- Transform scale: `scale(0.85)` mobile, `scale(0.9)` desktop

**Best Practices**:

- Keep visuals simple and focused
- Match Dienstplan visual aesthetic for consistency
- Use card-based widget design, not complex mockups
- Ensure visuals fill container completely (no spacing issues)
- Remove unnecessary decorative elements
- Use clean typography matching blog design system

**Removed Elements** (Zeiterfassung):

- Mobile phone frame/notch
- Complex header with back button
- Circular SVG timer (replaced with digital display)
- Bottom action button
- Alpine.js dependencies (no longer needed)

**Usage**:

```php
// In PillarHeroSideBySide.php component
<?php
$visual_component = __DIR__ . '/PillarVisualZeiterfassung.php';
if ($visual_component && file_exists($visual_component)) {
    include $visual_component;
}
?>
```

**Related Files**:

- `v2/components/blog/PillarHeroSideBySide.php` - Side-by-side layout wrapper
- `v2/css/blog-base.css`, `v2/css/blog-index.css`, `v2/css/blog-post.css` - Visual component styling (split by page type; see docs/content/blog/BLOG_CSS_SPLIT_MAINTENANCE.md)
- `v2/js/blog-visuals.js` - JavaScript (minimal, no Alpine.js needed)


## Heavy Instructions Moved

**CRITICAL:** The detailed instructions, edge cases, and massive data for this rule have been moved to optimize AI context.
You MUST read the full documentation before proceeding:
`docs/ai/rules-archive/blog-templates-components-full.md`

## Overview

Blog templates are PHP-based static pages that replace WordPress blog functionality. They use reusable components, JSON data files, and follow Ordio's established patterns for SEO, performance, and accessibility.

## Template Architecture

### Template Files

**Location**: `v2/pages/blog/`

**Templates**:

- `index.php` - Blog index (all posts)
- `category.php` - Category archive (reusable for lexikon, ratgeber, inside-ordio)
- `post.php` - Single post page
- `topic-hub.php` - Topic hub page

### Component Files

**Location**: `v2/components/blog/`

**Components**:

- `PostCard.php` - Post preview card
- `PostHeader.php` - Post header with featured image
- `PostContent.php` - Post content renderer
- `BlogTOC.php` - Table of contents component (dynamic heading extraction)
- `RelatedPosts.php` - Related posts section
- `Pagination.php` - Pagination navigation
- `CategoryNav.php` - Category navigation
- `Breadcrumbs.php` - Breadcrumb navigation
- `TopicHubHero.php` - Topic hub hero section

### Helper Functions

**Location**: `v2/config/blog-template-helpers.php`

**Key Functions**:

- `load_blog_post($category, $slug)` - Load single post
- `load_blog_posts_by_category($category, $limit, $offset)` - Load posts by category
- `load_all_blog_posts($limit, $offset)` - Load all posts
- `load_related_posts($post_slug, $category, $limit)` - Load related posts
- `format_blog_date($date_string, $format)` - Format dates
- `calculate_reading_time($word_count)` - Calculate reading time
- `get_category_label($category_slug)` - Get category label

### Meta & Schema Generators

**Location**: `v2/config/`

- `blog-meta-generator.php` - Meta tag generation
- `blog-schema-generator.php` - Schema markup generation

## URL Patterns

**Blog Index**: `/insights/` or `/insights/page/{page}/`
**Category Archive**: `/insights/{category}/` or `/insights/{category}/page/{page}/`
**Single Post**: `/insights/{category}/{slug}/`
**Topic Hub**: `/insights/topics/{topic_id}/`

**WordPress Archive Redirects** (handled in `.htaccess`):
- **Author Archives**: `/insights/author/{slug}/` → `/insights/` (301 redirect)
- **Date Archives**: `/insights/{year}/{month}/` → `/insights/` (301 redirect)
- **Rationale**: Redirect old WordPress archive pages to blog index to prevent duplicate content and consolidate SEO value
- **Documentation**: See `docs/content/blog/WORDPRESS_ARCHIVE_REDIRECTS.md` for complete details

**URL Extraction Pattern**:

```php
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$path = parse_url($request_uri, PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

if (count($segments) >= 3 && $segments[0] === 'insights') {
    $category = $segments[1] ?? '';
    $slug = $segments[2] ?? '';
}
```

## Data Structure

### Post JSON Format

**Location**: `v2/data/blog/posts/{category}/{slug}.json`

**Required Fields**:

- `slug`, `title`, `category`, `url`, `publication_date`, `content.html`

**Structure**:

```json
{
  "slug": "post-slug",
  "title": "Post Title",
  "category": "lexikon",
  "url": "/insights/lexikon/post-slug/",
  "publication_date": "2023-09-01T10:40:44+00:00",
  "content": {
    "html": "<p>...</p>",
    "text": "Plain text...",
    "word_count": 1214
  },
  "featured_image": {...},
  "excerpt": "...",
  "related_posts": [...],
  "topics": [...],
  "clusters": {...}
}
```

### Categories JSON

**Location**: `v2/data/blog/categories.json`

**Structure**:

```json
{
  "categories": [
    {
      "slug": "lexikon",
      "name": "Lexikon",
      "url": "/insights/lexikon/",
      "post_count": 22
    }
  ]
}
```

### Topics JSON

**Location**: `v2/data/blog/topics.json`

**Structure**:

```json
{
  "topics": [
    {
      "id": "zeiterfassung",
      "name": "Zeiterfassung",
      "hub_url": "/insights/topics/zeiterfassung/",
      "post_count": 69
    }
  ]
}
```

## Common Patterns

### Loading Post Data

```php
require_once __DIR__ . '/../../config/blog-template-helpers.php';

// Single post
$post = load_blog_post($category, $slug);

// Posts by category
$posts = load_blog_posts_by_category($category, $limit, $offset);

// All posts
$posts = load_all_blog_posts($limit, $offset);

// Related posts
$related_posts = load_related_posts($slug, $category, 14);
```

### Generating Meta Tags

```php
require_once __DIR__ . '/../../config/blog-meta-generator.php';

$meta_tags = render_blog_meta_tags($page_type, $data, $overrides);
echo $meta_tags;
```

### Generating Schema

```php
require_once __DIR__ . '/../../config/blog-schema-generator.php';

$schema = render_blog_schema($page_type, $data, $overrides);
echo $schema;
```

### Embedding JSON in Alpine.js Attributes

**CRITICAL**: When embedding JSON data in Alpine.js `x-data` attributes, always use `ENT_QUOTES` with `htmlspecialchars()`:

```php
// ✅ CORRECT - Use ENT_QUOTES to escape quotes for HTML attributes
x-data="blogIndexFilterData(<?php echo htmlspecialchars(json_encode($data), ENT_QUOTES, 'UTF-8'); ?>)"

// ❌ WRONG - ENT_NOQUOTES doesn't escape quotes, breaking HTML attributes
x-data="blogIndexFilterData(<?php echo htmlspecialchars(json_encode($data), ENT_NOQUOTES, 'UTF-8'); ?>)"
```

**Why `ENT_QUOTES`?**

- JSON contains double quotes that must be escaped for HTML attributes
- `ENT_QUOTES` converts `"` to `&quot;` so the HTML attribute doesn't break
- Browser automatically decodes HTML entities (`&quot;` → `"`, `&amp;` → `&`) when reading attributes
- JavaScript receives valid JSON after browser decoding
- Also escapes `&`, `<`, `>` for complete HTML safety

**Example**: JSON like `{"title":"Test"}` becomes `{&quot;title&quot;:&quot;Test&quot;}` in HTML, which the browser decodes back to `{"title":"Test"}` before JavaScript parses it.

**See**: `docs/development/ALPINE_JS_JSON_ENCODING.md` for complete best practices

### Template Structure

```php
<!doctype html>
<html lang="de">
<head>
    <?php
    $aosScript = "false";
    $swiperScript = "false";
    include '../../base/head.php';
    ?>
    <?php echo $meta_tags; ?>
    <?php echo $schema; ?>
</head>
<body class="antialiased">
    <?php
    $headerwidth = "max-w-7xl";
    include '../../base/header.php';
    ?>
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <!-- Content -->
    </main>
    <?php
    $color_fill = '#fff';
    $color_background = '#fbfbfb';
    $rotate = '0';
    $margin_bottom = 'mb-24';
    include '../../base/footer.php';
    ?>
</body>
</html>
```

## Backup Requirements

### Before Making Changes

**ALWAYS create a backup before:**

- Modifying blog post JSON files (`v2/data/blog/posts/`)
- Updating categories or topics data
- Bulk content updates
- Link structure changes
- Content extraction updates

### Backup Checklist

**Before Content Changes:**

- [ ] Create snapshot backup: `python3 scripts/blog/backup-blog-content.py --manual`
- [ ] Validate backup: `python3 scripts/blog/validate-backup.py docs/backups/blog-snapshots/<timestamp>`
- [ ] Document backup location and purpose

**After Content Changes:**

- [ ] Verify changes successful
- [ ] Test content loading
- [ ] Validate JSON files
- [ ] Create new backup if significant changes

### Backup Validation

**Required Validations:**

1. JSON syntax - All files parse correctly
2. File completeness - All expected files present
3. Checksums - File integrity verified

**Validation Commands:**

```bash
# Validate backup
python3 scripts/blog/validate-backup.py <backup_directory>

# Check backup status
python3 scripts/blog/check-backup-status.py
```

### Restoration

**If changes need to be reverted:**

```bash
# Restore from snapshot
python3 scripts/blog/restore-from-snapshot.py docs/backups/blog-snapshots/<timestamp>

# Dry run (test first)
python3 scripts/blog/restore-from-snapshot.py docs/backups/blog-snapshots/<timestamp> --dry-run
```

**See:** `.cursor/rules/blog-backup.mdc` for complete backup procedures

## Related Documentation

- `docs/content/blog/guides/TEMPLATE_DEVELOPMENT_GUIDE.md` - Complete development guide
- `docs/content/blog/reference/COMPONENT_API.md` - Component API documentation
- `docs/content/blog/reference/DATA_STRUCTURE_MAPPING.md` - Data structure mapping
- `docs/content/blog/BLOG_TEMPLATE_BEST_PRACTICES.md` - Best practices
- `docs/content/blog/guides/BACKUP_GUIDE.md` - Backup and restoration guide
- `docs/content/blog/BACKUP_PROCESS.md` - Backup procedures
- `docs/content/blog/reference/FRONTEND_PATTERNS_EXTRACTION.md` - Frontend patterns

## SEO Requirements

### Meta Tags

**Generated via**: `render_blog_meta_tags($page_type, $data, $overrides)`

**Page Types**:

- `'index'` - Blog index
- `'category'` - Category archive
- `'post'` - Single post
- `'topic_hub'` - Topic hub

**Title Tag Format** (single posts):

- Format: `{keyword phrase} - Ordio`
- Max 50 chars for keyword phrase (brand " - Ordio" added in HTML but not counted; Google often truncates displayed titles at ~50–55 chars)
- Differentiates from H1 to avoid duplicate H1/title SEO flags

**H1 Alignment**:

- H1 matches title tag content without brand suffix for SEO consistency
- Source: `get_blog_post_h1_title($post)` – uses meta title from seo-meta.json or post meta.title
- New posts: Set `meta.title` descriptive (e.g. "Keyword: Modifiers - Ordio"); H1 auto-derived
- See `docs/content/blog/BLOG_SEO_TITLE_STANDARDS.md` (H1 Alignment section)

**Required Meta Tags**:

- Title tag (50 chars keyword phrase + " - Ordio"; displayed portion optimized for ~50 chars)
- Meta description (155-160 chars)
- Canonical URL
- Open Graph tags (og:title, og:description, og:image, og:url, og:type)
- Twitter Card tags
- Article-specific tags (for posts: article:published_time, article:modified_time, article:author, article:section)

**Category Archives**:

- Robots: `noindex, follow` (category archives not indexed)

**Single Posts**:

- Robots: `index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1`
- Article tags: published_time, modified_time, author, section

### Schema Markup

**Generated via**: `render_blog_schema($page_type, $data, $overrides)`

**Schema Types**:

- **WebPage** - All pages (with breadcrumb reference)
- **CollectionPage** - Category archives
- **Article** - Single posts (with author Person reference)
- **BreadcrumbList** - All pages
- **Person** - Authors (for posts)
- **ImageObject** - Featured images
- **SpeakableSpecification** - Voice search optimization (for posts with paragraphs)

**Required Fields**:

- Article: headline, datePublished, dateModified, publisher, image, author
- WebPage: url, name, description, breadcrumb
- BreadcrumbList: itemListElement with position, name, item

**BreadcrumbList Schema Output Strategy**:

- **Format**: JSON-LD only (Google's preferred format)
- **Location**: Generated in `<head>` section via `render_blog_schema()` function
- **Microdata**: Disabled in Breadcrumbs component for blog pages (`$include_schema = false`)
- **Reason**: Prevents duplicate detection in Google Search Console (GSC shows "2 valid items" when both formats present)
- **Implementation**: 
  - JSON-LD: Generated by `blog-schema-generator.php` → output in `<head>` via `render_blog_schema()`
  - Microdata: Disabled in `Breadcrumbs.php` component by setting `$include_schema = false` in blog post templates
- **Best Practice**: Use one format per page (JSON-LD preferred) to avoid GSC warnings

**SpeakableSpecification Best Practices**:

- Use CSS selector only (not XPath) per Google recommendations
- CSS selector: `.post-content-inner p:first-of-type` (matches HTML structure)
- Only add if first paragraph exists in HTML content
- Never mix CSS selector and XPath (use one or the other)
- Validate with `php v2/scripts/blog/validate-schema-speakable.php` before deployment

## Validation Checklist

### Before Deploying

- [ ] All templates render without errors
- [ ] Meta tags present and correct
- [ ] Schema markup valid (Google Rich Results Test)
- [ ] SpeakableSpecification uses CSS selector only (no XPath)
- [ ] SpeakableSpecification CSS selector matches HTML structure
- [ ] Images optimized (WebP, srcset, lazy loading)
- [ ] Links work (internal and external)
- [ ] Pagination works correctly
- [ ] Page numbers display correctly (no duplicates, proper ellipsis)
- [ ] URL validation works (page 0, negative, beyond total redirects)
- [ ] SEO meta tags present (canonical, rel="prev", rel="next")
- [ ] Pagination schema markup present
- [ ] Keyboard navigation works (arrow keys, Home, End)
- [ ] Screen reader announcements work
- [ ] Mobile responsive (touch targets 44x44px minimum)
- [ ] Edge cases handled gracefully
- [ ] Responsive design works
- [ ] Accessibility tested (ARIA, keyboard navigation, color contrast)
- [ ] Performance tested (LCP < 2.5s, CLS < 0.1)
- [ ] SEO validated (meta tags, schema, canonical URLs)
- [ ] BreadcrumbList schema: Only JSON-LD present (no Microdata duplicates)
- [ ] Breadcrumbs component: `$include_schema = false` set for blog pages---
description: "Blog template patterns - Error handling, troubleshooting, common issues, and image verification. Part of blog templates system."
globs:
- v2/pages/blog/**/*.php
- v2/components/blog/**/*.php
- v2/config/blog-*.php
alwaysApply: false
relatedRules:
- blog-templates-core.mdc
- blog-templates-components.mdc
- blog-templates-seo.mdc
- blog-backup.mdc
relatedDocs:
- docs/content/blog/TROUBLESHOOTING_GUIDE.md
description: "Blog template patterns - Coordination file. This rule has been split into focused files. See relatedRules for specific topics. Migration from WordPress completed 2026-01-14."
globs:
- v2/pages/blog/**/*.php
- v2/components/blog/**/*.php
- v2/config/blog-*.php
alwaysApply: false
relatedRules:
- blog-templates-core.mdc
- blog-templates-components.mdc
- blog-templates-seo.mdc
- blog-templates-troubleshooting.mdc
- shared-patterns.mdc
- blog-backup.mdc
- blog-post-documentation.mdc
- content-clusters.mdc
- pillar-pages.mdc
- templates-pages.mdc
relatedDocs:
- docs/content/blog/guides/TEMPLATE_DEVELOPMENT_GUIDE.md
- docs/content/blog/reference/COMPONENT_API.md
- docs/content/blog/reference/DATA_STRUCTURE_MAPPING.md
- docs/content/blog/BLOG_TEMPLATE_BEST_PRACTICES.md
