` echo) so they participate in dependency * management like any other front-end script. */ final class IntegrationsOutput { private IntegrationsRepositoryInterface $integrations; public function __construct( IntegrationsRepositoryInterface $integrations ) { $this->integrations = $integrations; } public function register(): void { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_head_scripts' ) ); add_action( 'wp_head', array( $this, 'output_verification_tags' ), 1 ); // Runs on the 'wp_body_open' action (WP 5.2+) — themes that don't call // wp_body_open() in header.php simply won't get the noscript fallback; // this is a known, common limitation of GTM integrations in general, // not something we can work around without editing theme files. add_action( 'wp_body_open', array( $this, 'output_body_open' ) ); } public function enqueue_head_scripts(): void { if ( $this->should_skip() ) { return; } $settings = $this->integrations->get(); $this->enqueue_gtm_head( (string) $settings['gtm_container_id'] ); $this->enqueue_ga( (string) $settings['ga_measurement_id'] ); $this->enqueue_hotjar( (string) $settings['hotjar_site_id'] ); } /** * One `` per configured provider. Only fields * the user actually filled in are printed — an empty verification tag is * worse than none (Google reads it as a failed verification attempt). */ public function output_verification_tags(): void { if ( $this->should_skip() ) { return; } $settings = $this->integrations->get(); foreach ( IntegrationsRepository::verification_meta_names() as $option_key => $meta_name ) { $code = (string) ( $settings[ $option_key ] ?? '' ); if ( '' === $code ) { continue; } printf( '' . "\n", esc_attr( $meta_name ), esc_attr( $code ) ); } } public function output_body_open(): void { if ( $this->should_skip() ) { return; } $this->output_gtm_body( (string) $this->integrations->get()['gtm_container_id'] ); } private function should_skip(): bool { $settings = $this->integrations->get(); return (bool) $settings['exclude_admins'] && current_user_can( 'manage_options' ); } /** GTM's own loader snippet has no real external file — a handle-less registration is GTM/GA's own documented shape for wp_add_inline_script(). */ private function enqueue_gtm_head( string $container_id ): void { if ( '' === $container_id ) { return; } $id = esc_js( $container_id ); $handle = 'acez-seo-gtm'; wp_register_script( $handle, '', array(), ACEZ_SEO_VERSION, false ); wp_enqueue_script( $handle ); wp_add_inline_script( $handle, "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':" . "new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0]," . "j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=" . "'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);" . "})(window,document,'script','dataLayer','" . $id . "');" ); } private function output_gtm_body( string $container_id ): void { if ( '' === $container_id ) { return; } printf( "\n\n\n", esc_attr( $container_id ) ); } private function enqueue_ga( string $measurement_id ): void { if ( '' === $measurement_id ) { return; } $id = esc_js( $measurement_id ); $handle = 'acez-seo-ga4'; $src = add_query_arg( 'id', rawurlencode( $measurement_id ), 'https://www.googletagmanager.com/gtag/js' ); // 'strategy' => 'async' (WP 6.3+ script-loading-strategy API) matches // Google's own gtag.js snippet, which loads with the `async` attribute. // Version stays null on purpose: gtag.js is Google-hosted and versioned // by Google, so appending our own ?ver= would only bust their CDN cache. wp_enqueue_script( $handle, $src, array(), null, array( 'strategy' => 'async' ) ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Google-hosted gtag.js carries Google's own versioning; a ?ver= of ours would only bust their CDN cache. // MUST stay 'before', never 'after': WP_Scripts::filter_eligible_strategies() // treats any handle carrying an 'after' inline script as undelayable, which // silently drops the async attribute AND moves the whole gtag.js tag to the // footer (WP_Scripts::do_item()'s "intended strategy delayed but actual one // isn't" branch) — measurably later page-view hits. Defining dataLayer/gtag() // ahead of the loader is safe by design: the queued calls are replayed once // gtag.js finishes loading, which is how Google's own snippet works. wp_add_inline_script( $handle, "window.dataLayer = window.dataLayer || [];\n" . "function gtag(){dataLayer.push(arguments);}\n" . "gtag('js', new Date());\n" . "gtag('config', '" . $id . "');", 'before' ); } /** Hotjar's own loader snippet has no real external file upfront (it appends its own