diff --git a/crates/common/src/html_processor.rs b/crates/common/src/html_processor.rs
index e3d827f..b354e8b 100644
--- a/crates/common/src/html_processor.rs
+++ b/crates/common/src/html_processor.rs
@@ -1,4 +1,4 @@
-//! Simplified HTML processor that combines URL replacement and Prebid injection
+//! Simplified HTML processor that combines URL replacement and integration injection
//!
//! This module provides a `StreamProcessor` implementation for HTML content.
use std::cell::Cell;
@@ -191,10 +191,26 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
// Inject unified tsjs bundle once at the start of
element!("head", {
let injected_tsjs = injected_tsjs.clone();
+ let integrations = integration_registry.clone();
+ let patterns = patterns.clone();
+ let document_state = document_state.clone();
move |el| {
if !injected_tsjs.get() {
- let loader = tsjs::unified_script_tag();
- el.prepend(&loader, ContentType::Html);
+ let mut snippet = String::new();
+ let ctx = IntegrationHtmlContext {
+ request_host: &patterns.request_host,
+ request_scheme: &patterns.request_scheme,
+ origin_host: &patterns.origin_host,
+ document_state: &document_state,
+ };
+ // First inject the unified TSJS bundle (defines tsjs.setConfig, etc.)
+ snippet.push_str(&tsjs::unified_script_tag());
+ // Then add any integration-specific head inserts (e.g., mode config)
+ // These run after the bundle so tsjs API is available
+ for insert in integrations.head_inserts(&ctx) {
+ snippet.push_str(&insert);
+ }
+ el.prepend(&snippet, ContentType::Html);
injected_tsjs.set(true);
}
Ok(())
diff --git a/crates/common/src/integrations/registry.rs b/crates/common/src/integrations/registry.rs
index 7fe9e77..426b557 100644
--- a/crates/common/src/integrations/registry.rs
+++ b/crates/common/src/integrations/registry.rs
@@ -376,6 +376,14 @@ pub trait IntegrationHtmlPostProcessor: Send + Sync {
fn post_process(&self, html: &mut String, ctx: &IntegrationHtmlContext<'_>) -> bool;
}
+/// Trait for integration-provided HTML head injections.
+pub trait IntegrationHeadInjector: Send + Sync {
+ /// Identifier for logging/diagnostics.
+ fn integration_id(&self) -> &'static str;
+ /// Return HTML snippets to insert at the start of ``.
+ fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec;
+}
+
/// Registration payload returned by integration builders.
pub struct IntegrationRegistration {
pub integration_id: &'static str,
@@ -383,6 +391,7 @@ pub struct IntegrationRegistration {
pub attribute_rewriters: Vec>,
pub script_rewriters: Vec>,
pub html_post_processors: Vec>,
+ pub head_injectors: Vec>,
}
impl IntegrationRegistration {
@@ -405,6 +414,7 @@ impl IntegrationRegistrationBuilder {
attribute_rewriters: Vec::new(),
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
+ head_injectors: Vec::new(),
},
}
}
@@ -439,6 +449,12 @@ impl IntegrationRegistrationBuilder {
self
}
+ #[must_use]
+ pub fn with_head_injector(mut self, injector: Arc) -> Self {
+ self.registration.head_injectors.push(injector);
+ self
+ }
+
#[must_use]
pub fn build(self) -> IntegrationRegistration {
self.registration
@@ -460,6 +476,7 @@ struct IntegrationRegistryInner {
html_rewriters: Vec>,
script_rewriters: Vec>,
html_post_processors: Vec>,
+ head_injectors: Vec>,
}
impl Default for IntegrationRegistryInner {
@@ -474,6 +491,7 @@ impl Default for IntegrationRegistryInner {
html_rewriters: Vec::new(),
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
+ head_injectors: Vec::new(),
}
}
}
@@ -574,6 +592,9 @@ impl IntegrationRegistry {
inner
.html_post_processors
.extend(registration.html_post_processors.into_iter());
+ inner
+ .head_injectors
+ .extend(registration.head_injectors.into_iter());
}
}
@@ -662,6 +683,19 @@ impl IntegrationRegistry {
self.inner.html_post_processors.clone()
}
+ /// Collect HTML snippets for insertion at the start of ``.
+ #[must_use]
+ pub fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec {
+ let mut inserts = Vec::new();
+ for injector in &self.inner.head_injectors {
+ let mut next = injector.head_inserts(ctx);
+ if !next.is_empty() {
+ inserts.append(&mut next);
+ }
+ }
+ inserts
+ }
+
/// Provide a snapshot of registered integrations and their hooks.
#[must_use]
pub fn registered_integrations(&self) -> Vec {
@@ -711,6 +745,29 @@ impl IntegrationRegistry {
html_rewriters: attribute_rewriters,
script_rewriters,
html_post_processors: Vec::new(),
+ head_injectors: Vec::new(),
+ }),
+ }
+ }
+
+ #[cfg(test)]
+ pub fn from_rewriters_with_head_injectors(
+ attribute_rewriters: Vec>,
+ script_rewriters: Vec>,
+ head_injectors: Vec>,
+ ) -> Self {
+ Self {
+ inner: Arc::new(IntegrationRegistryInner {
+ get_router: Router::new(),
+ post_router: Router::new(),
+ put_router: Router::new(),
+ delete_router: Router::new(),
+ patch_router: Router::new(),
+ routes: Vec::new(),
+ html_rewriters: attribute_rewriters,
+ script_rewriters,
+ html_post_processors: Vec::new(),
+ head_injectors,
}),
}
}
@@ -765,6 +822,7 @@ impl IntegrationRegistry {
html_rewriters: Vec::new(),
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
+ head_injectors: Vec::new(),
}),
}
}