Skip to content

[Feature] Implement Font Collections via JSON Method #7

@krugazul

Description

@krugazul

Overview

Enable WordPress Font Collections in the block plugin scaffold using the JSON registration method introduced in WordPress 6.5. This feature allows plugins to provide curated sets of fonts that users can install and manage through the Font Library in the Site Editor.

Resources

Implementation Requirements

1. Create Font Collection JSON Files

Location: assets/fonts/

Create placeholder/template JSON files following the WordPress font collection schema:

{
  "$schema": "https://schemas.wp.org/trunk/font-collection.json",
  "font_families": [
    {
      "font_family_settings": {
        "name": "{{font_family_name}}",
        "fontFamily": "{{font_family_stack}}",
        "slug": "{{font_family_slug}}",
        "fontFace": [
          {
            "src": "{{font_src_url}}",
            "fontWeight": "{{font_weight}}",
            "fontStyle": "{{font_style}}",
            "fontFamily": "{{font_family_name}}",
            "preview": "{{preview_image_url}}"
          }
        ],
        "preview": "{{family_preview_url}}"
      },
      "categories": ["{{font_category}}"]
    }
  ]
}

Suggested Collections:

  • assets/fonts/{{slug}}-system-fonts.json - System font stacks (no files required)
  • assets/fonts/{{slug}}-custom-fonts.json - Plugin-specific custom fonts (optional)

2. Update Plugin Main File

File: {{slug}}.php

Add font collection registration method to the Core class or as a standalone function:

/**
 * Register plugin font collections.
 *
 * @since {{version}}
 */
public function register_font_collections() {
	// Register system fonts collection (if exists)
	$system_fonts_path = plugin_dir_path( __FILE__ ) . 'assets/fonts/{{slug}}-system-fonts.json';
	if ( file_exists( $system_fonts_path ) ) {
		wp_register_font_collection(
			'{{slug}}-system-fonts',
			array(
				'name'          => __( '{{plugin_name}} System Fonts', '{{text_domain}}' ),
				'description'   => __( 'A curated collection of system fonts optimized for {{plugin_name}}.', '{{text_domain}}' ),
				'font_families' => $system_fonts_path,
				'categories'    => array(
					array(
						'name' => __( 'Sans Serif', '{{text_domain}}' ),
						'slug' => 'sans-serif',
					),
					array(
						'name' => __( 'Serif', '{{text_domain}}' ),
						'slug' => 'serif',
					),
					array(
						'name' => __( 'Monospace', '{{text_domain}}' ),
						'slug' => 'monospace',
					),
					array(
						'name' => __( 'Handwriting', '{{text_domain}}' ),
						'slug' => 'handwriting',
					),
				),
			)
		);
	}

	// Register custom fonts collection (if exists)
	$custom_fonts_path = plugin_dir_path( __FILE__ ) . 'assets/fonts/{{slug}}-custom-fonts.json';
	if ( file_exists( $custom_fonts_path ) ) {
		wp_register_font_collection(
			'{{slug}}-custom-fonts',
			array(
				'name'          => __( '{{plugin_name}} Custom Fonts', '{{text_domain}}' ),
				'description'   => __( 'Custom font families designed for {{plugin_name}}.', '{{text_domain}}' ),
				'font_families' => $custom_fonts_path,
				'categories'    => array(
					array(
						'name' => __( 'Display', '{{text_domain}}' ),
						'slug' => 'display',
					),
				),
			)
		);
	}
}

Hook to init:

add_action( 'init', array( $this, 'register_font_collections' ) );

3. Update Mustache Variables Registry

File: scripts/mustache-variables-registry.json

Add new font collection variables to track:

{
  "font_collection_name": {
    "name": "font_collection_name",
    "category": "typography",
    "type": "string",
    "description": "Name of the font collection"
  },
  "font_collection_slug": {
    "name": "font_collection_slug",
    "category": "typography",
    "type": "slug",
    "description": "Slug for the font collection"
  },
  "font_collection_description": {
    "name": "font_collection_description",
    "category": "typography",
    "type": "string",
    "description": "Description of the font collection"
  },
  "font_family_name": {
    "name": "font_family_name",
    "category": "typography",
    "type": "string",
    "description": "Font family display name"
  },
  "font_family_slug": {
    "name": "font_family_slug",
    "category": "typography",
    "type": "slug",
    "description": "Font family slug"
  },
  "font_family_stack": {
    "name": "font_family_stack",
    "category": "typography",
    "type": "string",
    "description": "CSS font-family value with fallbacks"
  },
  "font_src_url": {
    "name": "font_src_url",
    "category": "typography",
    "type": "url",
    "description": "URL to font file"
  },
  "font_weight": {
    "name": "font_weight",
    "category": "typography",
    "type": "string",
    "description": "Font weight value"
  },
  "font_style": {
    "name": "font_style",
    "category": "typography",
    "type": "string",
    "description": "Font style (normal, italic)"
  },
  "font_category": {
    "name": "font_category",
    "category": "typography",
    "type": "string",
    "description": "Font category slug"
  },
  "preview_image_url": {
    "name": "preview_image_url",
    "category": "typography",
    "type": "url",
    "description": "URL to font preview image"
  },
  "family_preview_url": {
    "name": "family_preview_url",
    "category": "typography",
    "type": "url",
    "description": "URL to font family preview image"
  }
}

4. Update Plugin Generation Script

File: scripts/generate-plugin.js

Add font collection handling:

  1. Create assets/fonts/ directory during generation
  2. Process font collection JSON template files
  3. Replace mustache variables in JSON files
  4. Validate generated JSON against schema

Implementation additions:

// After plugin directory creation
const fontCollectionsDir = path.join(outputDir, 'assets/fonts');
fs.mkdirSync(fontCollectionsDir, { recursive: true });

// Process font collection templates
const fontCollectionFiles = [
  'assets/fonts/{{slug}}-system-fonts.json',
  'assets/fonts/{{slug}}-custom-fonts.json'
];

fontCollectionFiles.forEach(file => {
  const templatePath = path.join(scaffoldDir, file);
  if (fs.existsSync(templatePath)) {
    const content = fs.readFileSync(templatePath, 'utf8');
    const processed = replaceMustacheVariables(content, placeholders);
    const outputPath = path.join(outputDir, file.replace('{{slug}}', placeholders.slug));
    fs.writeFileSync(outputPath, processed);
    
    // Validate JSON
    try {
      JSON.parse(processed);
      logger.log('success', `Generated and validated: ${outputPath}`);
    } catch (error) {
      logger.log('error', `Invalid JSON in ${outputPath}: ${error.message}`);
    }
  }
});

5. Update Plugin Config Schema

File: .github/schemas/plugin-config.schema.json

Add font collection configuration options:

{
  "enable_font_collections": {
    "type": "boolean",
    "description": "Enable custom font collections for the plugin",
    "default": false
  },
  "font_collections": {
    "type": "array",
    "description": "Array of font collections to include",
    "items": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Display name of the font collection"
        },
        "slug": {
          "type": "string",
          "description": "Unique identifier for the collection",
          "pattern": "^[a-z0-9-]+$"
        },
        "description": {
          "type": "string",
          "description": "Description of the font collection"
        },
        "type": {
          "type": "string",
          "enum": ["system", "custom", "google"],
          "description": "Type of font collection"
        }
      },
      "required": ["name", "slug"]
    },
    "default": []
  }
}

6. Update Generation Agent

File: .github/agents/generate-plugin.agent.md

Add instructions for font collection generation:

## Font Collections

When generating plugins with font collections:

1. Check if `enable_font_collections` is true in config
2. Create `assets/fonts/` directory
3. Generate font collection JSON files based on `font_collections` array
4. For system fonts: use Modern Font Stacks as examples
5. For custom fonts: create placeholder structure
6. Validate all JSON against WordPress font collection schema
7. Add registration method to Core class or plugin main file
8. Include translatable strings for collection names and categories
9. Hook registration to `init` action

7. Update Core Class

File: inc/class-core.php

Add font collection registration method if using class-based structure:

/**
 * Register font collections.
 *
 * @since {{version}}
 */
public function register_font_collections() {
	// Registration code from section 2
}

Add to __construct() or init hooks:

add_action( 'init', array( $this, 'register_font_collections' ) );

8. Documentation Updates

Files to update:

  • docs/GENERATE_PLUGIN.md - Add font collections section
  • README.md - Mention font collections feature
  • USAGE.md - Add usage instructions for font collections
  • Create docs/FONT_COLLECTIONS.md - Detailed guide on:
    • Using font collections in plugins
    • Customizing font families
    • Schema validation
    • Examples (system fonts, web fonts)
    • Limitations and best practices
    • Plugin vs Theme considerations

9. Create Directory Structure

assets/
  fonts/
    .gitkeep
    {{slug}}-system-fonts.json  (template)
    {{slug}}-custom-fonts.json  (template)

10. Unit Tests

File: tests/test-font-collections.php

Add PHPUnit tests:

<?php
/**
 * Tests for font collection registration.
 *
 * @package {{slug}}
 */

class Test_Font_Collections extends WP_UnitTestCase {

	public function test_system_fonts_registered() {
		$collections = WP_Font_Library::get_instance()->get_font_collections();
		$this->assertArrayHasKey( '{{slug}}-system-fonts', $collections );
	}

	public function test_font_collection_has_valid_structure() {
		$collection = WP_Font_Library::get_instance()->get_font_collection( '{{slug}}-system-fonts' );
		$this->assertNotNull( $collection );
		$this->assertIsString( $collection->get_name() );
		$this->assertIsString( $collection->get_description() );
	}

	public function test_font_collection_json_valid() {
		$json_path = plugin_dir_path( dirname( __FILE__ ) ) . 'assets/fonts/{{slug}}-system-fonts.json';
		$this->assertFileExists( $json_path );
		
		$json_content = file_get_contents( $json_path );
		$this->assertJson( $json_content );
		
		$data = json_decode( $json_content, true );
		$this->assertArrayHasKey( 'font_families', $data );
		$this->assertIsArray( $data['font_families'] );
	}

	public function test_font_collection_slug_prefixed() {
		$collections = WP_Font_Library::get_instance()->get_font_collections();
		foreach ( $collections as $slug => $collection ) {
			if ( strpos( $slug, '{{slug}}-' ) === 0 ) {
				$this->assertStringStartsWith( '{{slug}}-', $slug );
			}
		}
	}
}

11. Example System Fonts Collection

Create a default example using Modern Font Stacks:

{
  "$schema": "https://schemas.wp.org/trunk/font-collection.json",
  "font_families": [
    {
      "font_family_settings": {
        "fontFamily": "system-ui, sans-serif",
        "slug": "system-ui",
        "name": "System UI"
      },
      "categories": ["sans-serif"]
    },
    {
      "font_family_settings": {
        "fontFamily": "Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif",
        "slug": "transitional",
        "name": "Transitional"
      },
      "categories": ["serif"]
    },
    {
      "font_family_settings": {
        "fontFamily": "'Nimbus Mono PS', 'Courier New', monospace",
        "slug": "monospace-slab-serif",
        "name": "Monospace Slab Serif"
      },
      "categories": ["monospace", "serif"]
    },
    {
      "font_family_settings": {
        "fontFamily": "'Segoe Print', 'Bradley Hand', Chilanka, TSCu_Comic, casual, cursive",
        "slug": "handwritten",
        "name": "Handwritten"
      },
      "categories": ["handwriting"]
    }
  ]
}

12. Plugin-Specific Considerations

Namespace Conflicts:

  • Ensure font collection slugs are prefixed with plugin slug
  • Example: my-plugin-system-fonts not just system-fonts

Plugin Deactivation:

  • Consider unregistering collections on deactivation
  • Add uninstall cleanup in uninstall.php or uninstall-{{slug}}.php

Uninstall Cleanup:

// In uninstall-{{slug}}.php
if ( function_exists( 'wp_unregister_font_collection' ) ) {
	wp_unregister_font_collection( '{{slug}}-system-fonts' );
	wp_unregister_font_collection( '{{slug}}-custom-fonts' );
}

Checklist

  • Create assets/fonts/ directory structure
  • Create font collection JSON template files with mustache placeholders
  • Add font collection registration to Core class or plugin main file
  • Update mustache variables registry
  • Update plugin generation script to process font collections
  • Update plugin config schema with font collection options
  • Update generate-plugin agent instructions
  • Create documentation (FONT_COLLECTIONS.md)
  • Update existing docs (GENERATE_PLUGIN.md, README.md, USAGE.md)
  • Add PHPUnit tests
  • Add generation script tests
  • Add uninstall cleanup
  • Test with wp-env
  • Validate JSON against schema
  • Test with basic and advanced wizard modes
  • Add examples for both system and custom fonts
  • Ensure proper plugin slug prefixing
  • Test plugin activation/deactivation

Notes

  • JSON Method Only: We're using the JSON method exclusively as it's cleaner for templates and follows best practices
  • Placeholders: All files use mustache variables for plugin-specific values
  • Validation: All generated JSON must be validated against the WordPress schema
  • Optional Feature: Font collections should be optional in the wizard (default: disabled for plugins)
  • System Fonts First: Prioritize system fonts as they don't require file hosting
  • Plugin Review Compliance: Be mindful of WordPress.org Plugin Review guidelines regarding external resources
  • Translation Ready: All collection names and descriptions must be translatable
  • Slug Prefixing: Always prefix font collection slugs with plugin slug to avoid conflicts
  • Cleanup: Provide proper uninstall cleanup for font collections
  • Plugin vs Theme: Unlike themes, plugins should use plugin_dir_path() instead of get_theme_file_path()
  • Class-Based Structure: If using the Core class pattern, add as a method; otherwise add as function in main plugin file
  • Init Hook: Always hook font collection registration to init action with appropriate priority

Related Issues

  • Relates to typography features in block patterns
  • May impact block editor font controls
  • Consider integration with SCF JSON for font metadata
  • Consider relationship with pattern typography settings

Plugin-Specific Use Cases

  • Form Builders: Custom fonts for form typography
  • Page Builders: Font collections for design systems
  • E-commerce: Brand-specific font collections
  • Membership Sites: Exclusive font collections for members
  • LMS Plugins: Educational content typography
  • Portfolio Plugins: Designer-curated font sets

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions