# WORDPRESS_COMPATIBILITY.md — WordPress & Plugin Compatibility

## Overview

This document covers compatibility requirements for WordPress core systems and popular plugins. Reference this when building the font registry class and integration points.

---

## WordPress Version Requirements

**Minimum: WordPress 6.5**

WordPress 6.5 introduced the Font Library API which this plugin depends on. Earlier versions will not work.

```php
// Check on activation
register_activation_hook(__FILE__, function() {
    if (version_compare(get_bloginfo('version'), '6.5', '<')) {
        deactivate_plugins(plugin_basename(__FILE__));
        wp_die(
            'Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.',
            'Plugin Activation Error',
            ['back_link' => true]
        );
    }
});

// Also check on admin init (in case WP was downgraded)
add_action('admin_init', function() {
    if (version_compare(get_bloginfo('version'), '6.5', '<')) {
        deactivate_plugins(plugin_basename(__FILE__));
        add_action('admin_notices', function() {
            echo '<div class="error"><p>Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.</p></div>';
        });
    }
});
```

---

## WordPress Font Library API

### How It Works

WordPress 6.5+ stores fonts using custom post types:

| Post Type | Purpose |
|-----------|---------|
| `wp_font_family` | Font family (e.g., "Open Sans") |
| `wp_font_face` | Individual weight/style variant (child of family) |

Fonts are stored in `wp-content/fonts/` by default.

### Getting the Fonts Directory

```php
// ALWAYS use this function, never hardcode paths
$font_dir = wp_get_font_dir();

// Returns:
[
    'path'    => '/var/www/html/wp-content/fonts',
    'url'     => 'https://example.com/wp-content/fonts',
    'subdir'  => '',
    'basedir' => '/var/www/html/wp-content/fonts',
    'baseurl' => 'https://example.com/wp-content/fonts',
]
```

### Registering a Font Family

```php
/**
 * Register a font family with WordPress Font Library.
 *
 * @param string $font_name Display name (e.g., "Open Sans")
 * @param string $font_slug Slug (e.g., "open-sans")
 * @param array  $files     Array of downloaded file data
 * @return int|WP_Error Font family post ID or error
 */
function mlf_register_font_family($font_name, $font_slug, $files) {
    // Check if font already exists
    $existing = get_posts([
        'post_type'      => 'wp_font_family',
        'name'           => $font_slug,
        'posts_per_page' => 1,
        'post_status'    => 'publish',
    ]);
    
    if (!empty($existing)) {
        return new WP_Error('font_exists', 'Font family already installed');
    }
    
    // Build font family settings
    $font_family_settings = [
        'name'       => $font_name,
        'slug'       => $font_slug,
        'fontFamily' => sprintf('"%s", sans-serif', $font_name),
        'fontFace'   => [],
    ];
    
    // Add each font face
    foreach ($files as $file) {
        $font_dir = wp_get_font_dir();
        $relative_path = str_replace($font_dir['path'], '', $file['path']);
        
        $font_family_settings['fontFace'][] = [
            'fontFamily' => $font_name,
            'fontWeight' => $file['weight'],
            'fontStyle'  => $file['style'],
            'src'        => 'file:.' . $font_dir['basedir'] . $relative_path,
        ];
    }
    
    // Create font family post
    $family_id = wp_insert_post([
        'post_type'    => 'wp_font_family',
        'post_title'   => $font_name,
        'post_name'    => $font_slug,
        'post_status'  => 'publish',
        'post_content' => wp_json_encode($font_family_settings),
    ]);
    
    if (is_wp_error($family_id)) {
        return $family_id;
    }
    
    // Mark as imported by our plugin (for identification)
    update_post_meta($family_id, '_mlf_imported', '1');
    update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
    
    // Create font face posts (children)
    foreach ($files as $file) {
        $font_dir = wp_get_font_dir();
        
        $face_settings = [
            'fontFamily' => $font_name,
            'fontWeight' => $file['weight'],
            'fontStyle'  => $file['style'],
            'src'        => 'file:.' . $font_dir['baseurl'] . '/' . basename($file['path']),
        ];
        
        wp_insert_post([
            'post_type'    => 'wp_font_face',
            'post_parent'  => $family_id,
            'post_status'  => 'publish',
            'post_content' => wp_json_encode($face_settings),
        ]);
    }
    
    // Clear font caches
    delete_transient('wp_font_library_fonts');
    
    return $family_id;
}
```

### Deleting a Font Family

```php
/**
 * Delete a font family and its files.
 *
 * @param int $family_id Font family post ID
 * @return bool|WP_Error True on success, error on failure
 */
function mlf_delete_font_family($family_id) {
    $family = get_post($family_id);
    
    if (!$family || $family->post_type !== 'wp_font_family') {
        return new WP_Error('not_found', 'Font family not found');
    }
    
    // Verify it's one we imported
    if (get_post_meta($family_id, '_mlf_imported', true) !== '1') {
        return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
    }
    
    // Get font faces
    $faces = get_children([
        'post_parent' => $family_id,
        'post_type'   => 'wp_font_face',
    ]);
    
    $font_dir = wp_get_font_dir();
    
    // Delete font face files and posts
    foreach ($faces as $face) {
        $settings = json_decode($face->post_content, true);
        
        if (isset($settings['src'])) {
            // Convert file:. URL to path
            $src = $settings['src'];
            $src = str_replace('file:.', '', $src);
            
            // Handle both URL and path formats
            if (strpos($src, $font_dir['baseurl']) !== false) {
                $file_path = str_replace($font_dir['baseurl'], $font_dir['path'], $src);
            } else {
                $file_path = $font_dir['path'] . '/' . basename($src);
            }
            
            // Validate path before deletion
            if (mlf_validate_font_path($file_path) && file_exists($file_path)) {
                wp_delete_file($file_path);
            }
        }
        
        wp_delete_post($face->ID, true);
    }
    
    // Delete family post
    wp_delete_post($family_id, true);
    
    // Clear caches
    delete_transient('wp_font_library_fonts');
    
    return true;
}
```

### Listing Installed Fonts

```php
/**
 * Get all fonts imported by this plugin.
 *
 * @return array Array of font data
 */
function mlf_get_imported_fonts() {
    $fonts = get_posts([
        'post_type'      => 'wp_font_family',
        'posts_per_page' => 100,
        'post_status'    => 'publish',
        'meta_key'       => '_mlf_imported',
        'meta_value'     => '1',
    ]);
    
    $result = [];
    
    foreach ($fonts as $font) {
        $settings = json_decode($font->post_content, true);
        
        // Get variants
        $faces = get_children([
            'post_parent' => $font->ID,
            'post_type'   => 'wp_font_face',
        ]);
        
        $variants = [];
        foreach ($faces as $face) {
            $face_settings = json_decode($face->post_content, true);
            $variants[] = [
                'weight' => $face_settings['fontWeight'] ?? '400',
                'style'  => $face_settings['fontStyle'] ?? 'normal',
            ];
        }
        
        $result[] = [
            'id'         => $font->ID,
            'name'       => $settings['name'] ?? $font->post_title,
            'slug'       => $settings['slug'] ?? $font->post_name,
            'variants'   => $variants,
            'import_date' => get_post_meta($font->ID, '_mlf_import_date', true),
        ];
    }
    
    return $result;
}
```

---

## Gutenberg FSE Integration

### How Fonts Appear in the Editor

Once registered via the Font Library API, fonts automatically appear in:

1. **Global Styles** → Typography → Font dropdown
2. **Block settings** → Typography → Font dropdown (when per-block typography is enabled)

No additional integration code is needed — WordPress handles this automatically.

### Theme.json Compatibility

**DO NOT:**
- Directly modify theme.json
- Filter `wp_theme_json_data_theme` to inject fonts (let Font Library handle it)
- Override global styles CSS directly

**DO:**
- Use the Font Library API (post types)
- Let WordPress generate CSS custom properties
- Trust the system

### CSS Custom Properties

When a font is applied in Global Styles, WordPress generates:

```css
body {
    --wp--preset--font-family--open-sans: "Open Sans", sans-serif;
}
```

And applies it:

```css
body {
    font-family: var(--wp--preset--font-family--open-sans);
}
```

Our plugin doesn't need to touch this — it's automatic.

---

## WooCommerce Compatibility

### HPOS (High-Performance Order Storage)

WooCommerce's HPOS moves order data from post meta to custom tables. We must declare compatibility.

```php
// Declare HPOS compatibility
add_action('before_woocommerce_init', function() {
    if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
            'custom_order_tables',
            __FILE__,
            true
        );
    }
});
```

### Why We're Compatible

Our plugin:
- Does NOT interact with orders at all
- Does NOT query wp_posts for order data
- Does NOT use wp_postmeta for order data
- Only uses wp_font_family and wp_font_face post types

We're inherently compatible because we don't touch WooCommerce data.

### Frontend Considerations

**DO NOT:**
- Override `.woocommerce` class styles
- Override `.wc-block-*` styles
- Target cart/checkout elements specifically

**DO:**
- Let WooCommerce elements inherit from body/heading fonts
- Let global styles cascade naturally

WooCommerce product titles, descriptions, and other text will naturally inherit the fonts set via Global Styles. No special handling needed.

---

## Wordfence Compatibility

### Potential Concerns

1. **Outbound requests** to Google Fonts during import
2. **AJAX endpoints** for admin actions
3. **File operations** in wp-content

### Why We're Compatible

**Outbound Requests:**
- Only occur during admin import (user-initiated action)
- Target well-known domains (fonts.googleapis.com, fonts.gstatic.com)
- Use standard `wp_remote_get()` which Wordfence allows
- No runtime external requests on frontend

**AJAX Endpoints:**
- Use standard `admin-ajax.php` (not custom endpoints)
- Include proper nonces
- Follow WordPress patterns that Wordfence expects

**File Operations:**
- Write only to `wp-content/fonts/` (WordPress default directory)
- Use WordPress Filesystem API
- Don't create executable files

### Testing with Wordfence

Test these scenarios with Wordfence active:

- [ ] Learning Mode: Import should succeed
- [ ] Enabled Mode: Import should succeed
- [ ] Rate Limiting: Admin AJAX not blocked
- [ ] Firewall: No false positives on font download

---

## LearnDash Compatibility

### Overview

LearnDash is a WordPress LMS that uses:
- Custom post types (courses, lessons, topics, quizzes)
- Custom templates
- Focus Mode (distraction-free learning)

### Why We're Compatible

Our plugin:
- Doesn't touch LearnDash post types
- Doesn't modify LearnDash templates
- Doesn't inject CSS on frontend
- Lets Global Styles cascade to LearnDash content

LearnDash course content, lesson text, and quiz questions will inherit the fonts set in Global Styles automatically.

### Focus Mode Consideration

LearnDash Focus Mode uses its own template. Fonts set via Global Styles will apply because:
- Focus Mode still loads theme.json styles
- CSS custom properties cascade to all content
- No special handling needed

**DO NOT:**
- Target `.learndash-*` classes specifically
- Override Focus Mode styles
- Inject custom CSS for LearnDash

---

## WPForms Compatibility

### Overview

WPForms renders forms via shortcodes and blocks. Form styling is handled by WPForms.

### Why We're Compatible

- Form labels and text inherit from body font
- We don't override `.wpforms-*` classes
- No JavaScript conflicts (we have no frontend JS)

### Consideration

If a user wants form text in a different font, they should use WPForms' built-in styling options or custom CSS — not expect our plugin to handle it.

---

## General Best Practices

### What We Hook Into

```php
// Admin menu
add_action('admin_menu', [$this, 'register_menu']);

// Admin assets (only on our page)
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);

// AJAX handlers
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);

// WooCommerce HPOS compatibility
add_action('before_woocommerce_init', [$this, 'declare_hpos_compatibility']);
```

### What We DON'T Hook Into

```php
// NO frontend hooks
// add_action('wp_enqueue_scripts', ...);   // DON'T DO THIS
// add_action('wp_head', ...);               // DON'T DO THIS
// add_action('wp_footer', ...);             // DON'T DO THIS

// NO theme modification hooks
// add_filter('wp_theme_json_data_theme', ...);  // Let Font Library handle it

// NO WooCommerce hooks
// add_action('woocommerce_*', ...);         // DON'T DO THIS

// NO content filters
// add_filter('the_content', ...);           // DON'T DO THIS
```

---

## Conflict Debugging

If a user reports a conflict, check:

### 1. Plugin Load Order

Our plugin should load with default priority. Check if another plugin is:
- Modifying the Font Library
- Overriding font CSS
- Filtering theme.json

### 2. CSS Specificity

If fonts aren't applying:
- Check browser DevTools for CSS cascade
- Look for more specific selectors overriding global styles
- Check for `!important` declarations

### 3. Cache Issues

Font changes not appearing:
- Clear browser cache
- Clear any caching plugins (WP Rocket, W3TC, etc.)
- Clear CDN cache if applicable
- WordPress transients: `delete_transient('wp_font_library_fonts')`

### 4. JavaScript Errors

If admin page isn't working:
- Check browser console for JS errors
- Look for conflicts with other admin scripts
- Verify jQuery isn't being dequeued

---

## Compatibility Checklist

Before releasing:

### WordPress Core
- [ ] Works on WordPress 6.5
- [ ] Works on WordPress 6.6+
- [ ] Font Library API integration works
- [ ] Fonts appear in Global Styles
- [ ] Fonts apply correctly on frontend

### WooCommerce
- [ ] HPOS compatibility declared
- [ ] No errors in WooCommerce status page
- [ ] Product pages render correctly with custom fonts
- [ ] Cart/Checkout not affected

### Wordfence
- [ ] Import works with firewall enabled
- [ ] No blocked requests
- [ ] No false positive security alerts

### LearnDash
- [ ] Course content inherits fonts
- [ ] Focus Mode renders correctly
- [ ] No JavaScript conflicts

### WPForms
- [ ] Forms render correctly
- [ ] No styling conflicts

### Other
- [ ] No PHP errors in debug.log
- [ ] No JavaScript errors in console
- [ ] Admin page loads correctly
- [ ] No memory issues during import
