# 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 '

Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.

'; }); } }); ``` --- ## 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