Source: classes/SettingsManifest.php

<?php
/**
 * Settings schema manifest.
 *
 * @package PoweredCache
 */

namespace PoweredCache;

/**
 * Builds an API/UI friendly settings schema manifest.
 *
 * @since 4.0.0
 */
class SettingsManifest {

	const FORMAT         = 'powered-cache-settings-manifest';
	const FORMAT_VERSION = '1.0';

	/**
	 * Build a settings manifest from the current schema.
	 *
	 * @param array $context Runtime context used by dynamic defaults.
	 * @param bool  $include_deprecated Whether deprecated fields should be included.
	 *
	 * @return array
	 */
	public static function build( array $context = array(), $include_deprecated = true ) {
		return array(
			'format'         => self::FORMAT,
			'format_version' => self::FORMAT_VERSION,
			'plugin_version' => defined( 'POWERED_CACHE_VERSION' ) ? POWERED_CACHE_VERSION : '',
			'sections'       => self::sections(),
			'fields'         => self::fields( $context, $include_deprecated ),
		);
	}

	/**
	 * Return ordered settings sections.
	 *
	 * @return array
	 */
	public static function sections() {
		$sections = array(
			'cache'             => array(
				'label'       => 'Cache',
				'description' => 'Control full-page cache, object cache, and browser-facing cache behavior.',
				'order'       => 10,
			),
			'advanced'          => array(
				'label'       => 'Advanced',
				'description' => 'Fine tune exclusions, query strings, cookies, and server configuration.',
				'order'       => 20,
			),
			'file_optimization' => array(
				'label'       => 'File Optimization',
				'description' => 'Reduce render-blocking HTML, CSS, JavaScript, and font overhead.',
				'order'       => 30,
			),
			'media'             => array(
				'label'       => 'Media',
				'description' => 'Optimize images, embeds, and lazy loading behavior.',
				'order'       => 40,
			),
			'cdn'               => array(
				'label'       => 'CDN',
				'description' => 'Route static assets through your CDN hostnames.',
				'order'       => 50,
			),
			'preload'           => array(
				'label'       => 'Preload',
				'description' => 'Warm important URLs and prepare critical resources before visitors arrive.',
				'order'       => 60,
			),
			'database'          => array(
				'label'       => 'Database',
				'description' => 'Clean up database overhead and schedule recurring maintenance.',
				'order'       => 70,
			),
			'integrations'      => array(
				'label'       => 'Integrations',
				'description' => 'Connect external services and WordPress runtime integrations.',
				'order'       => 80,
			),
			'advisor'           => array(
				'label'       => 'Advisor',
				'description' => 'Review detected site signals, recommended next steps, and runtime health checks.',
				'order'       => 85,
			),
			'misc'              => array(
				'label'       => 'Tools',
				'description' => 'Manage import/export, cache footprint, async cleanup, tracking, and developer mode.',
				'order'       => 90,
			),
		);

		/**
		 * Filter settings sections shown in the admin app and submenu deep links.
		 *
		 * @hook powered_cache_settings_sections
		 *
		 * @param {array} $sections Settings section metadata.
		 *
		 * @return {array} New value.
		 *
		 * @since 4.0.0
		 */
		return apply_filters( 'powered_cache_settings_sections', $sections );
	}

	/**
	 * Return manifest fields keyed by setting name.
	 *
	 * @param array $context Runtime context used by dynamic defaults.
	 * @param bool  $include_deprecated Whether deprecated fields should be included.
	 *
	 * @return array
	 */
	public static function fields( array $context = array(), $include_deprecated = true ) {
		$fields = array();
		$order  = 10;

		foreach ( SettingsSchema::fields( $context ) as $key => $field ) {
			if ( ! $include_deprecated && ! empty( $field['deprecated'] ) ) {
				continue;
			}

			$fields[ $key ] = self::field( $key, $field, $order, $context );
			$order         += 10;
		}

		return $fields;
	}

	/**
	 * Return one manifest field.
	 *
	 * @param string $key Setting key.
	 * @param array  $field Schema field.
	 * @param int    $order Field display order.
	 * @param array  $context Runtime context used by dynamic defaults.
	 *
	 * @return array
	 */
	private static function field( $key, array $field, $order, array $context ) {
		$metadata = self::field_metadata( $key, $field );
		$editable = SettingsSchema::can_edit( $key, $context );
		$manifest = array(
			'key'           => $key,
			'label'         => $metadata['label'],
			'description'   => $metadata['description'],
			'type'          => $field['type'],
			'control'       => $metadata['control'] ? $metadata['control'] : self::control( $field ),
			'control_label' => $metadata['control_label'],
			'default'       => $field['default'],
			'section'       => $field['section'],
			'group'         => $metadata['group'],
			'order'         => $order,
			'sanitizer'     => $field['sanitizer'],
			'premium'       => (bool) $field['premium'],
			'sensitive'     => in_array( $key, SettingsRepository::sensitive_keys(), true ),
			'editable'      => $editable,
			'locked'        => ! $editable,
			'lock_reason'   => SettingsSchema::lock_reason( $key, $context ),
			'dependencies'  => array_values( $field['dependencies'] ),
			'enum'          => array_values( $field['enum'] ),
			'enum_labels'   => self::enum_labels( $field['enum'] ),
			'deprecated'    => (bool) $field['deprecated'],
		);

		foreach ( array( 'docs_fragment', 'docs_path', 'docs_url', 'input_type', 'max', 'min', 'options', 'placeholder', 'visible_when', 'zone_key', 'zone_options' ) as $metadata_key ) {
			if ( isset( $metadata[ $metadata_key ] ) ) {
				$manifest[ $metadata_key ] = $metadata[ $metadata_key ];
			}
		}

		if ( $manifest['premium'] ) {
			$manifest['upgrade'] = array(
				'label'       => 'Upgrade to Premium',
				'description' => $metadata['upgrade_description'],
			);
		}

		return $manifest;
	}

	/**
	 * Return UI control type for a schema field.
	 *
	 * @param array $field Schema field.
	 *
	 * @return string
	 */
	private static function control( array $field ) {
		if ( SettingsSchema::TYPE_BOOLEAN === $field['type'] ) {
			return 'toggle';
		}

		if ( SettingsSchema::TYPE_ENUM === $field['type'] ) {
			return 'select';
		}

		if ( SettingsSchema::TYPE_INTEGER === $field['type'] ) {
			return 'number';
		}

		if ( SettingsSchema::TYPE_ARRAY === $field['type'] ) {
			return 'list';
		}

		if ( SettingsSchema::SANITIZE_TEXTAREA === $field['sanitizer'] ) {
			return 'textarea';
		}

		return 'text';
	}

	/**
	 * Return field labels and descriptions for UI consumers.
	 *
	 * @param string $key Setting key.
	 * @param array  $field Schema field.
	 *
	 * @return array
	 */
	private static function field_metadata( $key, array $field ) {
		$metadata = array(
			'label'               => self::label_from_key( $key ),
			'control'             => '',
			'control_label'       => '',
			'description'         => '',
			'group'               => $field['section'],
			'upgrade_description' => 'Unlock this optimization in Powered Cache Premium.',
		);

		$overrides = self::metadata_overrides();

		if ( isset( $overrides[ $key ] ) ) {
			$metadata = array_merge( $metadata, $overrides[ $key ] );
		}

		if ( 'object_cache' === $key && array( 'off' ) === array_values( $field['enum'] ) ) {
			$metadata['description'] = 'No supported persistent object cache PHP extension was detected on this server.';
		}

		return $metadata;
	}

	/**
	 * Return field metadata overrides keyed by setting key.
	 *
	 * @return array
	 */
	private static function metadata_overrides() {
		return array(
			'enable_page_cache'              => array(
				'label'         => 'Page Cache',
				'control_label' => 'Serve cached pages',
				'description'   => 'Serve cached HTML for faster repeat and anonymous visits.',
				'group'         => 'Core cache',
				'docs_path'     => 'page-caching',
			),
			'object_cache'                   => array(
				'label'       => 'Object Cache',
				'description' => 'Use a persistent object cache backend for dynamic WordPress data.',
				'group'       => 'Core cache',
				'docs_path'   => 'object-caching',
			),
			'cache_mobile'                   => array(
				'label'         => 'Mobile Cache',
				'control_label' => 'Cache mobile visits',
				'description'   => 'Cache visits from mobile devices.',
				'group'         => 'Core cache',
			),
			'cache_mobile_separate_file'     => array(
				'label'         => 'Separate Mobile Cache',
				'control_label' => 'Use a separate mobile cache',
				'description'   => 'Create mobile-specific cache files when the site serves different mobile markup.',
				'group'         => 'Core cache',
			),
			'loggedin_user_cache'            => array(
				'label'         => 'Logged-in User Cache',
				'control_label' => 'Cache logged-in visits',
				'description'   => 'Serve cached pages to logged-in users only when the site output is safe to share.',
				'group'         => 'Core cache',
			),
			'gzip_compression'               => array(
				'label'         => 'Gzip Compression',
				'control_label' => 'Serve compressed files',
				'description'   => 'Serve compressed cache files when supported by the server.',
				'group'         => 'Delivery',
			),
			'cache_timeout'                  => array(
				'control'     => 'duration',
				'label'       => 'Cache Lifespan',
				'description' => 'Set how long cached pages stay fresh.',
				'group'       => 'Delivery',
			),
			'auto_configure_htaccess'        => array(
				'label'         => '.htaccess Configuration',
				'control_label' => 'Automatically configure .htaccess',
				'description'   => 'Let Powered Cache write recommended Apache rewrite rules when settings change.',
				'group'         => 'Server configuration',
				'docs_path'     => 'rewrite-file-optimizer',
			),
			'rejected_user_agents'           => array(
				'label'         => 'Rejected User Agents',
				'description'   => 'Never serve cached pages to matching browsers, bots, or crawlers.',
				'group'         => 'Cache exclusions',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'rejected-user-agents',
			),
			'rejected_cookies'               => array(
				'label'         => 'Rejected Cookies',
				'description'   => 'Bypass page cache when a visitor has one of these cookies.',
				'group'         => 'Cache exclusions',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'rejected-cookies',
			),
			'rejected_referrers'             => array(
				'label'         => 'Rejected Referrers',
				'description'   => 'Bypass page cache when traffic comes from matching referrer URLs.',
				'group'         => 'Cache exclusions',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'rejected-referrers',
			),
			'vary_cookies'                   => array(
				'label'         => 'Vary Cookies',
				'description'   => 'Create separate cache variants when these cookies are present.',
				'group'         => 'Cache variants',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'vary-cookies',
			),
			'rejected_uri'                   => array(
				'label'         => 'Never Cache URLs',
				'description'   => 'Exclude pages from page cache, one path or regex per line. Each rule must match the whole path, so use * as a wildcard, for example /cart* or *preview=*.',
				'group'         => 'Cache exclusions',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'ignored-pages',
			),
			'ignored_query_strings'          => array(
				'label'         => 'Ignored Query Strings',
				'description'   => 'Ignore matching query parameters and serve the standard cached page.',
				'group'         => 'Query strings',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'ignored-query-strings',
			),
			'cache_query_strings'            => array(
				'label'         => 'Cache Query Strings',
				'description'   => 'Create separate cache files for the listed query parameters.',
				'group'         => 'Query strings',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'cache-query-strings',
			),
			'purge_additional_pages'         => array(
				'label'         => 'Purge Additional Pages',
				'description'   => 'Clear extra related URLs when content changes.',
				'group'         => 'Purge behavior',
				'docs_path'     => '/advanced-options/',
				'docs_fragment' => 'purge-additional-pages',
			),
			'minify_html'                    => array(
				'label'       => 'Minify HTML',
				'description' => 'Remove unnecessary whitespace from generated HTML.',
				'group'       => 'HTML',
			),
			'minify_html_dom_optimization'   => array(
				'label'         => 'HTML DOM Optimization',
				'control_label' => 'Optimize HTML output',
				'description'   => 'Apply safer HTML output cleanup after HTML minification is enabled.',
				'group'         => 'HTML',
			),
			'combine_google_fonts'           => array(
				'label'         => 'Combine Google Fonts',
				'control_label' => 'Combine Google Fonts requests',
				'description'   => 'Reduce multiple Google Fonts stylesheet requests into a single request when possible.',
				'group'         => 'Fonts',
			),
			'swap_google_fonts_display'      => array(
				'label'         => 'Font Display Swap',
				'control_label' => 'Use display swap for Google Fonts',
				'description'   => 'Add display swap behavior so text can render while web fonts load.',
				'group'         => 'Fonts',
			),
			'use_bunny_fonts'                => array(
				'label'         => 'Bunny Fonts',
				'control_label' => 'Serve Google Fonts through Bunny Fonts',
				'description'   => 'Use Bunny Fonts as a privacy-friendly drop-in source for Google Fonts.',
				'group'         => 'Fonts',
			),
			'self_host_google_fonts'         => array(
				'label'               => 'Self-host Google Fonts',
				'control_label'       => 'Cache Google Fonts locally',
				'description'         => 'Download Google Fonts stylesheets and font files into the local cache directory and serve them from this site.',
				'group'               => 'Fonts',
				'docs_path'           => 'self-host-google-fonts',
				'upgrade_description' => 'Premium can cache Google Fonts locally to reduce third-party font requests.',
			),
			'minify_css'                     => array(
				'label'       => 'Minify CSS',
				'description' => 'Reduce CSS file size before delivery.',
				'group'       => 'CSS',
			),
			'combine_css'                    => array(
				'label'       => 'Combine CSS',
				'description' => 'Combine CSS files when it improves delivery on the site.',
				'group'       => 'CSS',
			),
			'critical_css'                   => array(
				'label'               => 'Critical CSS',
				'description'         => 'Generate and inline above-the-fold CSS for important templates.',
				'group'               => 'CSS',
				'docs_path'           => 'critical-css',
				'upgrade_description' => 'Premium can generate Critical CSS automatically for key templates and posts.',
			),
			'critical_css_additional_files'  => array(
				'label'       => 'Critical CSS Additional Files',
				'description' => 'Include extra stylesheet URLs when Critical CSS is generated.',
				'group'       => 'CSS',
				'docs_path'   => 'critical-css',
			),
			'critical_css_excluded_files'    => array(
				'label'       => 'Critical CSS Excluded Files',
				'description' => 'Skip matching stylesheet URLs during Critical CSS generation.',
				'group'       => 'CSS',
				'docs_path'   => 'critical-css',
			),
			'critical_css_appended_content'  => array(
				'label'       => 'Append to Critical CSS',
				'description' => 'Append CSS that must always be included with generated Critical CSS.',
				'group'       => 'CSS',
				'control'     => 'textarea',
				'docs_path'   => 'critical-css',
			),
			'critical_css_fallback'          => array(
				'label'       => 'Fallback Critical CSS',
				'description' => 'Use this CSS when generated Critical CSS is unavailable for a page.',
				'group'       => 'CSS',
				'control'     => 'textarea',
				'docs_path'   => 'critical-css',
			),
			'excluded_css_files'             => array(
				'label'       => 'Excluded CSS Files',
				'description' => 'Keep matching CSS files out of CSS minify or combine processing.',
				'group'       => 'CSS',
			),
			'remove_unused_css'              => array(
				'label'               => 'Remove Unused CSS',
				'description'         => 'Generate lean CSS payloads by removing rules unused on the page.',
				'group'               => 'CSS',
				'docs_path'           => 'remove-unused-css',
				'upgrade_description' => 'Premium can generate used CSS and reduce page weight without manual cleanup.',
			),
			'ucss_safelist'                  => array(
				'label'       => 'Unused CSS Safelist',
				'description' => 'Always keep matching selectors when unused CSS is generated.',
				'group'       => 'CSS',
				'docs_path'   => 'remove-unused-css',
			),
			'ucss_excluded_files'            => array(
				'label'       => 'Unused CSS Excluded Files',
				'description' => 'Skip matching stylesheets during unused CSS generation.',
				'group'       => 'CSS',
				'docs_path'   => 'remove-unused-css',
			),
			'css_optimization_disabled_sources' => array(
				'control' => 'hidden',
				'label'   => 'Paused CSS Compatibility Sources',
				'group'   => 'CSS',
			),
			'minify_js'                      => array(
				'label'       => 'Minify JavaScript',
				'description' => 'Reduce JavaScript file size before delivery.',
				'group'       => 'JavaScript',
			),
			'combine_js'                     => array(
				'label'       => 'Combine JavaScript',
				'description' => 'Combine JavaScript files when it improves delivery on the site.',
				'group'       => 'JavaScript',
			),
			'excluded_js_files'              => array(
				'label'       => 'Excluded JavaScript Files',
				'description' => 'Keep matching scripts out of JavaScript minify or combine processing.',
				'group'       => 'JavaScript',
			),
			'js_defer'                       => array(
				'label'       => 'Defer JavaScript',
				'description' => 'Load JavaScript without blocking initial page rendering.',
				'group'       => 'JavaScript',
			),
			'js_defer_exclusions'            => array(
				'label'       => 'Defer JavaScript Exclusions',
				'description' => 'Keep matching scripts in their original loading order.',
				'group'       => 'JavaScript',
			),
			'js_delay'                       => array(
				'label'       => 'Delay JavaScript',
				'description' => 'Delay selected scripts until user interaction or timeout.',
				'group'       => 'JavaScript',
			),
			'js_delay_exclusions'            => array(
				'label'       => 'Delay JavaScript Exclusions',
				'description' => 'Allow matching scripts to run without waiting for interaction.',
				'group'       => 'JavaScript',
			),
			'js_delay_timeout'               => array(
				'label'       => 'Delay Timeout',
				'description' => 'Set the fallback delay before delayed scripts are allowed to run.',
				'group'       => 'JavaScript',
				'min'         => 0,
			),
			'rewrite_file_optimizer'         => array(
				'label'         => 'File Optimizer Rewrite Rules',
				'control_label' => 'Write optimizer rewrite rules',
				'description'   => 'Let Powered Cache write Apache rewrite rules for optimized CSS and JavaScript files.',
				'group'         => 'Server configuration',
				'docs_path'     => 'rewrite-file-optimizer',
			),
			'enable_image_optimization'      => array(
				'label'               => 'Image Optimization',
				'description'         => 'Optimize images on demand through the Powered Cache image delivery service.',
				'group'               => 'Images',
				'docs_path'           => 'image-optimization',
				'upgrade_description' => 'Premium adds on-the-fly WebP/AVIF image optimization backed by fast CDN delivery.',
			),
			'image_optimizer_preferred_format' => array(
				'label'               => 'Image Format Preference',
				'control'             => 'select',
				'description'         => 'Choose whether optimized images should prefer the automatic AVIF-first flow or WebP.',
				'group'               => 'Images',
				'options'             => array(
					''     => 'Automatic (AVIF when supported)',
					'webp' => 'Prefer WebP',
				),
				'upgrade_description' => 'Premium can serve optimized images in modern formats through the delivery network.',
			),
			'add_missing_image_dimensions'   => array(
				'label'               => 'Automatic Image Dimensions',
				'description'         => 'Add missing width and height attributes to improve layout stability.',
				'group'               => 'Images',
				'docs_path'           => 'image-dimensions',
				'upgrade_description' => 'Premium can add missing image dimensions automatically to improve CLS.',
			),
			'enable_lazy_load'               => array(
				'label'         => 'Lazy Load',
				'control_label' => 'Delay offscreen media',
				'description'   => 'Delay images and embeds until they are close to the viewport.',
				'group'         => 'Lazy loading',
				'docs_path'     => 'enable-lazy-load',
			),
			'lazy_load_post_content'         => array(
				'label'         => 'Post Content',
				'control_label' => 'Lazy load post content media',
				'description'   => 'Apply lazy loading to images and embeds inside post content.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_images'               => array(
				'label'         => 'Images',
				'control_label' => 'Lazy load images',
				'description'   => 'Delay eligible image loading until images approach the viewport.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_background_images'    => array(
				'label'         => 'CSS Background Images',
				'control_label' => 'Lazy load inline background images',
				'description'   => 'Delay eligible inline background images until their elements approach the viewport.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_iframes'              => array(
				'label'         => 'Iframes',
				'control_label' => 'Lazy load iframes',
				'description'   => 'Delay eligible iframe loading until embeds approach the viewport.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_widgets'              => array(
				'label'         => 'Widgets',
				'control_label' => 'Lazy load widget media',
				'description'   => 'Apply lazy loading to eligible images and embeds inside widgets.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_post_thumbnail'       => array(
				'label'         => 'Post Thumbnails',
				'control_label' => 'Lazy load featured images',
				'description'   => 'Delay eligible featured images when they are not needed immediately.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_avatars'              => array(
				'label'         => 'Avatars',
				'control_label' => 'Lazy load avatars',
				'description'   => 'Delay comment and profile avatar images until they are near the viewport.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_youtube'              => array(
				'label'         => 'YouTube Embeds',
				'control_label' => 'Replace YouTube embeds with thumbnails',
				'description'   => 'Replace YouTube iframes with lightweight thumbnails until visitors interact.',
				'group'         => 'Lazy loading',
			),
			'lazy_load_skip_first_nth_img'   => array(
				'label'       => 'Skip First Images',
				'description' => 'Avoid lazy loading the first images so above-the-fold media can remain fast.',
				'group'       => 'Lazy loading',
				'min'         => 0,
				'max'         => 30,
			),
			'lazy_load_exclusions'           => array(
				'label'       => 'Lazy Load Exclusions',
				'description' => 'Exclude matching images, iframes, classes, filenames, or domains from lazy loading.',
				'group'       => 'Lazy loading',
				'docs_path'   => 'enable-lazy-load',
			),
			'disable_wp_lazy_load'           => array(
				'label'         => 'WordPress Native Lazy Load',
				'control_label' => 'Disable WordPress native lazy load',
				'description'   => 'Turn off WordPress native lazy loading when Powered Cache should manage it instead.',
				'group'         => 'WordPress media',
				'docs_path'     => 'disable-wordpress-lazy-load',
			),
			'disable_wp_embeds'              => array(
				'label'         => 'WordPress Embeds',
				'control_label' => 'Disable WordPress embeds',
				'description'   => 'Prevent WordPress URLs from turning into heavier embed iframes.',
				'group'         => 'WordPress media',
				'docs_path'     => 'disable-wordpress-embeds',
			),
			'disable_emoji_scripts'          => array(
				'label'         => 'Emoji Scripts',
				'control_label' => 'Remove emoji scripts',
				'description'   => 'Remove WordPress emoji detection scripts without removing emoji characters.',
				'group'         => 'WordPress media',
				'docs_path'     => 'remove-emoji-scripts',
			),
			'enable_cdn'                     => array(
				'label'         => 'CDN Delivery',
				'control_label' => 'Rewrite asset URLs',
				'description'   => 'Rewrite static asset URLs to configured CDN hostnames.',
				'group'         => 'CDN',
				'docs_path'     => 'cdn-integration',
			),
			'cdn_hostname'                   => array(
				'label'        => 'CDN Hostnames',
				'control'      => 'cdn_zones',
				'description'  => 'Map each CDN hostname to all assets or to a specific file type.',
				'group'        => 'CDN',
				'docs_path'    => 'cdn-integration',
				'zone_key'     => 'cdn_zone',
				'zone_options' => self::cdn_zone_options(),
			),
			'cdn_zone'                       => array(
				'label'       => 'CDN Zone',
				'control'     => 'hidden',
				'description' => 'CDN zone mappings are managed with hostnames.',
				'group'       => 'CDN',
			),
			'cdn_rejected_files'             => array(
				'label'       => 'CDN Excluded Files',
				'description' => 'Keep matching full URLs or absolute paths on the origin instead of the CDN.',
				'group'       => 'CDN',
				'docs_path'   => 'cdn-integration',
			),
			'enable_cache_preload'           => array(
				'label'         => 'Cache Preload',
				'control_label' => 'Warm cache automatically',
				'description'   => 'Warm selected URLs before visitors request them.',
				'group'         => 'Preload',
				'docs_path'     => 'cache-preload',
			),
			'preload_homepage'               => array(
				'label'         => 'Preload Homepage',
				'control_label' => 'Include the homepage',
				'description'   => 'Warm the homepage when cache preloading runs.',
				'group'         => 'Preload',
			),
			'preload_public_posts'           => array(
				'label'         => 'Preload Public Posts',
				'control_label' => 'Include public posts',
				'description'   => 'Warm public post, page, and custom post type URLs.',
				'group'         => 'Preload',
			),
			'preload_public_tax'             => array(
				'label'         => 'Preload Public Taxonomies',
				'control_label' => 'Include public archives',
				'description'   => 'Warm public category, tag, and taxonomy archive URLs.',
				'group'         => 'Preload',
			),
			'enable_sitemap_preload'         => array(
				'label'               => 'Sitemap Preload',
				'control_label'       => 'Read URLs from sitemaps',
				'description'         => 'Use sitemap URLs as an additional preload source.',
				'group'               => 'Preload',
				'docs_path'           => 'sitemap-preload',
				'upgrade_description' => 'Premium can discover and warm URLs from XML sitemaps automatically.',
			),
			'preload_request_interval'       => array(
				'label'       => 'Preload Request Interval',
				'description' => 'Set the delay between preload requests to reduce server pressure.',
				'group'       => 'Preload',
				'min'         => 1,
			),
			'preload_sitemap'                => array(
				'label'       => 'Sitemap URLs',
				'description' => 'Add XML sitemap URLs that should feed the preload queue.',
				'group'       => 'Preload',
				'docs_path'   => 'sitemap-preload',
			),
			'prefetch_dns'                   => array(
				'label'       => 'DNS Prefetch',
				'description' => 'Resolve external hostnames earlier so later resource requests can start faster.',
				'group'       => 'Resource hints',
				'docs_path'   => 'prefetch-dns',
			),
			'preconnect_resource'            => array(
				'label'       => 'Preconnect',
				'description' => 'Open early connections for the most important third-party origins.',
				'group'       => 'Resource hints',
				'docs_path'   => 'preconnect-resources',
			),
			'preload_fonts'                  => array(
				'label'       => 'Preload Fonts',
				'description' => 'Load critical font files earlier to reduce late text rendering shifts.',
				'group'       => 'Resource hints',
				'docs_path'   => 'preload-fonts',
				'placeholder' => "https://example.com/wp-content/themes/theme/assets/fonts/inter.woff2\n/wp-content/uploads/fonts/brand.woff",
			),
			'enable_lcp_optimization'        => array(
				'label'               => 'LCP Optimization',
				'description'         => 'Detect and prioritize the likely Largest Contentful Paint resource.',
				'group'               => 'Critical resources',
				'docs_path'           => 'lcp-optimization',
				'upgrade_description' => 'Premium can prioritize critical LCP images and resources automatically.',
			),
			'prefetch_links'                 => array(
				'label'               => 'Link Prefetching',
				'control_label'       => 'Prefetch in-viewport links',
				'description'         => 'Speed up likely next page views during browser idle time.',
				'group'               => 'Critical resources',
				'upgrade_description' => 'Premium can prefetch likely next pages so repeat navigation feels instant.',
			),
			'enable_scheduled_db_cleanup'    => array(
				'label'               => 'Scheduled Cleanups',
				'control_label'       => 'Clean database on schedule',
				'description'         => 'Run selected database cleanup tasks on a schedule.',
				'group'               => 'Scheduling',
				'upgrade_description' => 'Premium can automatically clean selected database overhead on a recurring schedule.',
			),
			'scheduled_db_cleanup_frequency' => array(
				'label'               => 'Database Cleanup Frequency',
				'description'         => 'Choose how often scheduled database cleanups should run.',
				'group'               => 'Scheduling',
				'upgrade_description' => 'Premium can run automatic database cleanups hourly, daily, weekly, or monthly.',
			),
			'db_cleanup_post_revisions'      => array(
				'label'         => 'Post Revisions',
				'control_label' => 'Clean post revisions',
				'description'   => 'Remove saved post revisions during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_auto_drafts'         => array(
				'label'         => 'Auto Drafts',
				'control_label' => 'Clean auto drafts',
				'description'   => 'Remove automatically created draft posts during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_trashed_posts'       => array(
				'label'         => 'Trashed Posts',
				'control_label' => 'Clean trashed posts',
				'description'   => 'Remove trashed posts and pages during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_spam_comments'       => array(
				'label'         => 'Spam Comments',
				'control_label' => 'Clean spam comments',
				'description'   => 'Remove spam comments during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_trashed_comments'    => array(
				'label'         => 'Trashed Comments',
				'control_label' => 'Clean trashed comments',
				'description'   => 'Remove trashed comments during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_expired_transients'  => array(
				'label'         => 'Expired Transients',
				'control_label' => 'Clean expired transients',
				'description'   => 'Remove expired transient data during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_all_transients'      => array(
				'label'         => 'All Transients',
				'control_label' => 'Clean all transients',
				'description'   => 'Remove all transient data during database cleanup.',
				'group'         => 'Cleanup tasks',
			),
			'db_cleanup_optimize_tables'     => array(
				'label'         => 'Optimize Tables',
				'control_label' => 'Optimize database tables',
				'description'   => 'Run table optimization for supported database tables.',
				'group'         => 'Cleanup tasks',
			),
			'enable_cloudflare'              => array(
				'label'         => 'Cloudflare',
				'control_label' => 'Purge Cloudflare cache',
				'description'   => 'Purge Cloudflare when Powered Cache clears site cache.',
				'group'         => 'CDN and proxy',
				'docs_path'     => 'cloudflare',
			),
			'cloudflare_api_token'           => array(
				'label'       => 'Cloudflare API Token',
				'description' => 'Use a scoped API token for Cloudflare cache purge requests.',
				'group'       => 'CDN and proxy',
				'docs_url'    => 'https://dash.cloudflare.com/profile/api-tokens',
				'input_type'  => 'password',
			),
			'cloudflare_email'               => array(
				'label'       => 'Cloudflare Email',
				'description' => 'Legacy Cloudflare account email used with a global API key.',
				'group'       => 'CDN and proxy',
			),
			'cloudflare_api_key'             => array(
				'label'       => 'Cloudflare API Key',
				'description' => 'Legacy Cloudflare global API key. Prefer an API token when possible.',
				'group'       => 'CDN and proxy',
				'input_type'  => 'password',
			),
			'cloudflare_zone'                => array(
				'label'       => 'Cloudflare Zone ID',
				'description' => 'The Cloudflare zone that should be purged when cache is cleared.',
				'group'       => 'CDN and proxy',
			),
			'enable_sucuri'                  => array(
				'label'               => 'Sucuri',
				'control_label'       => 'Purge Sucuri cache',
				'description'         => 'Purge Sucuri Website Firewall cache when Powered Cache clears site cache.',
				'group'               => 'CDN and proxy',
				'upgrade_description' => 'Premium can purge Sucuri Website Firewall cache automatically.',
			),
			'sucuri_api_key'                 => array(
				'label'       => 'Sucuri API Key',
				'description' => 'Enter the Sucuri Website Firewall API key for cache purge requests.',
				'group'       => 'CDN and proxy',
				'input_type'  => 'password',
			),
			'sucuri_api_secret'              => array(
				'label'       => 'Sucuri API Secret',
				'description' => 'Enter the Sucuri Website Firewall API secret. The value is encrypted and hidden after saving.',
				'group'       => 'CDN and proxy',
				'input_type'  => 'password',
			),
			'enable_heartbeat'               => array(
				'label'         => 'Heartbeat Control',
				'control_label' => 'Manage Heartbeat activity',
				'description'   => 'Adjust WordPress Heartbeat behavior in the dashboard, editor, and site front end.',
				'group'         => 'WordPress runtime',
			),
			'heartbeat_dashboard_status'     => array(
				'label'       => 'Dashboard Heartbeat',
				'description' => 'Choose how Heartbeat should behave on WordPress dashboard screens.',
				'group'       => 'Heartbeat',
			),
			'heartbeat_dashboard_interval'   => array_merge(
				self::heartbeat_interval_metadata( 'heartbeat_dashboard_status' ),
				array(
					'label'       => 'Dashboard Interval',
					'description' => 'Set the dashboard Heartbeat interval in seconds.',
				)
			),
			'heartbeat_editor_status'        => array(
				'label'       => 'Editor Heartbeat',
				'description' => 'Choose how Heartbeat should behave in the post editor.',
				'group'       => 'Heartbeat',
			),
			'heartbeat_editor_interval'      => array_merge(
				self::heartbeat_interval_metadata( 'heartbeat_editor_status' ),
				array(
					'label'       => 'Editor Interval',
					'description' => 'Set the editor Heartbeat interval in seconds.',
				)
			),
			'heartbeat_frontend_status'      => array(
				'label'       => 'Frontend Heartbeat',
				'description' => 'Choose how Heartbeat should behave for frontend visits.',
				'group'       => 'Heartbeat',
			),
			'heartbeat_frontend_interval'    => array_merge(
				self::heartbeat_interval_metadata( 'heartbeat_frontend_status' ),
				array(
					'label'       => 'Frontend Interval',
					'description' => 'Set the frontend Heartbeat interval in seconds.',
				)
			),
			'enable_varnish'                 => array(
				'label'               => 'Varnish',
				'control_label'       => 'Purge Varnish cache',
				'description'         => 'Purge Varnish when cache is cleared.',
				'group'               => 'Reverse proxy',
				'upgrade_description' => 'Premium can purge external Varnish caches whenever Powered Cache clears page cache.',
			),
			'varnish_ip'                     => array(
				'label'       => 'Varnish IP Addresses',
				'description' => 'Add Varnish server IP addresses that should receive purge requests.',
				'group'       => 'Reverse proxy',
			),
			'cache_footprint'                => array(
				'label'         => 'Cache Footprint',
				'control_label' => 'Track cache footprint',
				'description'   => 'Track cache size so the dashboard can report cache footprint accurately.',
				'group'         => 'Tools',
			),
			'async_cache_cleaning'           => array(
				'label'         => 'Async Cache Cleaning',
				'control_label' => 'Clean cache asynchronously',
				'description'   => 'Process cache cleanup in the background when supported by the site.',
				'group'         => 'Tools',
			),
			'dev_mode'                       => array(
				'label'         => 'Development Mode',
				'control_label' => 'Bypass cache temporarily',
				'description'   => 'Temporarily bypass cache behavior while working on the site.',
				'group'         => 'Developer tools',
			),
			'enable_google_tracking'         => array(
				'label'               => 'Google Tracking Optimization',
				'control_label'       => 'Optimize Google tracking scripts',
				'description'         => 'Apply Premium delivery optimizations for supported Google tracking scripts.',
				'group'               => 'Tracking',
				'upgrade_description' => 'Premium can optimize supported Google tracking scripts for better frontend performance.',
			),
			'enable_fb_tracking'             => array(
				'label'               => 'Meta Pixel Optimization',
				'control_label'       => 'Optimize Meta Pixel scripts',
				'description'         => 'Apply Premium delivery optimizations for supported Meta Pixel scripts.',
				'group'               => 'Tracking',
				'upgrade_description' => 'Premium can optimize supported Meta Pixel scripts for better frontend performance.',
			),
		);
	}

	/**
	 * Return shared Heartbeat interval field metadata.
	 *
	 * @param string $status_key Status setting key.
	 *
	 * @return array
	 */
	private static function heartbeat_interval_metadata( $status_key ) {
		return array(
			'group'        => 'Heartbeat',
			'min'          => 15,
			'max'          => 120,
			'visible_when' => array(
				array(
					'key'   => $status_key,
					'value' => 'modify',
				),
			),
		);
	}

	/**
	 * Return CDN zone options for the settings app.
	 *
	 * @return array
	 */
	private static function cdn_zone_options() {
		if ( function_exists( '\PoweredCache\Utils\cdn_zones' ) ) {
			return \PoweredCache\Utils\cdn_zones();
		}

		return array(
			'all'   => 'All files',
			'image' => 'Images',
			'js'    => 'JavaScript',
			'css'   => 'CSS',
		);
	}

	/**
	 * Build enum labels keyed by enum value.
	 *
	 * @param array $values Enum values.
	 *
	 * @return array
	 */
	private static function enum_labels( array $values ) {
		$labels = array();

		foreach ( $values as $value ) {
			$labels[ $value ] = self::label_from_key( $value );
		}

		return $labels;
	}

	/**
	 * Convert a schema key to a readable label.
	 *
	 * @param string $key Schema key.
	 *
	 * @return string
	 */
	private static function label_from_key( $key ) {
		$key = str_replace( array( '_', '-' ), ' ', (string) $key );

		return ucwords( $key );
	}
}