monorepo/native/wordpress/maple-fonts-wp/WORDPRESS_COMPATIBILITY.md
2026-01-30 22:33:40 -05:00

15 KiB

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.

// 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

// 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

/**
 * 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

/**
 * 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

/**
 * 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:

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

And applies it:

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.

// 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

// 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

// 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