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

560 lines
15 KiB
Markdown

# 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