array_merge( $b, [ * 'name' => 'Agency SEO Insights', * 'logo_url' => 'https://agency.example/logo.svg', // square, shown at 18–40 px * 'site_url' => 'https://agency.example', * 'docs_url' => 'https://agency.example/help/', * 'show_upsell' => false, // hide "See Pro", upgrade boxes and PRO badges * 'show_vendor' => false, // hide links to clusterlane.app * ] ) ); * * What follows the brand: the admin menu name and icon, page titles, the * dock and Dashboard logo, notices, the support report, drafts created from * briefs, and — when the name differs — every translated text that says * "Clusterlane" (via the gettext filter for this text domain). JavaScript gets * the same values as window.clusterlaneBrand. * * Internal names are not branded: the text domain, the clusterlane_ prefix, REST * routes and the AI prompts stay as they are. */ final class Brand { const DEFAULTS = [ 'name' => 'Clusterlane', 'logo_url' => '', // '' = the built-in station mark 'site_url' => 'https://clusterlane.app', 'docs_url' => 'https://clusterlane.app/docs/', 'guides_url' => 'https://clusterlane.app/guides/', 'pricing_url' => 'https://clusterlane.app/pricing/', 'support_url' => 'https://clusterlane.app/', 'show_upsell' => true, 'show_vendor' => true, ]; /** Line and ring colours of the mark (the plugin's Pillar orange and ink). */ const ORANGE = '#E76C2E'; const INK = '#16223A'; private static ?array $brand = null; /** The brand, filtered and sanitized once per request. */ public static function get(): array { if ( self::$brand !== null ) return self::$brand; $b = (array) apply_filters( 'clusterlane_brand', self::DEFAULTS ); $b = array_merge( self::DEFAULTS, array_intersect_key( $b, self::DEFAULTS ) ); foreach ( [ 'logo_url', 'site_url', 'docs_url', 'guides_url', 'pricing_url', 'support_url' ] as $k ) { $b[ $k ] = $b[ $k ] !== '' ? esc_url_raw( (string) $b[ $k ] ) : ''; } $b['name'] = trim( wp_strip_all_tags( (string) $b['name'] ) ) ?: self::DEFAULTS['name']; $b['show_upsell'] = (bool) $b['show_upsell']; $b['show_vendor'] = (bool) $b['show_vendor']; return self::$brand = $b; } /** Forget the cached brand (tests, or after a settings change in the same request). */ public static function reset(): void { self::$brand = null; } public static function name(): string { return self::get()['name']; } /** True when the plugin is shown under another name than Clusterlane. */ public static function is_white_label(): bool { return self::name() !== self::DEFAULTS['name']; } /** site | docs | guides | pricing | support — '' when vendor links are hidden. */ public static function url( string $which ): string { $b = self::get(); $url = (string) ( $b[ $which . '_url' ] ?? '' ); // Pricing keeps its older filter (Freemius hooks in here). if ( $which === 'pricing' ) $url = (string) apply_filters( 'clusterlane_upgrade_url', $url ); if ( ! $b['show_vendor'] && str_starts_with( $url, self::DEFAULTS['site_url'] ) ) return ''; return $url; } public static function show_upsell(): bool { return self::get()['show_upsell']; } /** * The mark: a station (ring) on a line — the same mark as on clusterlane.app. * $ring is the ring's stroke, $fill its centre (the background it sits on). * With a custom logo_url, an of that logo is returned instead. */ public static function mark( int $size = 24, string $ring = self::INK, string $fill = '#fff' ): string { $logo = self::get()['logo_url']; if ( $logo !== '' ) { return ''; } return ''; } /** Allowed markup for mark() — for wp_kses(). */ public static function kses(): array { return [ 'svg' => [ 'class' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'aria-hidden' => true, 'focusable' => true ], 'path' => [ 'd' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'fill' => true ], 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true ], 'img' => [ 'class' => true, 'src' => true, 'width' => true, 'height' => true, 'alt' => true, 'aria-hidden' => true ], ]; } /** Print the mark, escaped. */ public static function mark_e( int $size = 24, string $ring = self::INK, string $fill = '#fff' ): void { echo wp_kses( self::mark( $size, $ring, $fill ), self::kses() ); } /** * Admin menu icon: the mark in one colour ("black" is the WordPress * convention — the admin colour scheme tints it). A custom logo is used * as is. */ public static function menu_icon(): string { $logo = self::get()['logo_url']; if ( $logo !== '' ) return $logo; $svg = '' . '' . '' . ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- data URI for the menu icon } /** For JavaScript (window.clusterlaneBrand). */ public static function to_js(): array { return [ 'name' => self::name(), 'whiteLabel' => self::is_white_label(), 'showUpsell' => self::show_upsell(), 'urls' => [ 'site' => self::url( 'site' ), 'docs' => self::url( 'docs' ), 'guides' => self::url( 'guides' ), 'pricing' => self::url( 'pricing' ), 'support' => self::url( 'support' ), ], ]; } /** * Under another name, translated texts that say "Clusterlane" say that * name instead — one filter for this text domain, no per-string work. */ public static function register_hooks(): void { if ( ! self::is_white_label() ) return; $swap = fn( $text ) => str_replace( self::DEFAULTS['name'], self::name(), (string) $text ); add_filter( 'gettext_clusterlane', $swap, 20 ); add_filter( 'ngettext_clusterlane', $swap, 20 ); add_filter( 'gettext_with_context_clusterlane', $swap, 20 ); // The Plugins screen shows the brand too (the support report keeps the // real plugin name on purpose — it is for support). add_filter( 'all_plugins', function ( $plugins ) { $file = plugin_basename( CLUSTERLANE_FILE ); if ( isset( $plugins[ $file ] ) ) { $plugins[ $file ]['Name'] = self::name(); $plugins[ $file ]['Title'] = self::name(); $plugins[ $file ]['PluginURI'] = self::url( 'site' ); if ( ! self::get()['show_vendor'] ) { $plugins[ $file ]['Author'] = ''; $plugins[ $file ]['AuthorURI'] = ''; $plugins[ $file ]['AuthorName'] = ''; } } return $plugins; } ); } }