Skip to content

Commit d5e6ff1

Browse files
committed
Improvements and docs
1 parent e0781e5 commit d5e6ff1

File tree

6 files changed

+142
-9
lines changed

6 files changed

+142
-9
lines changed

crates/common/src/html_processor.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ impl HtmlProcessorConfig {
111111
}
112112
}
113113

114-
/// Create an HTML processor with URL replacement and optional Prebid injection
114+
/// Create an HTML processor with URL replacement and integration hooks
115115
#[must_use]
116116
pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcessor {
117117
let post_processors = config.integrations.html_post_processors();
@@ -474,6 +474,7 @@ mod tests {
474474
use super::*;
475475
use crate::integrations::{
476476
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
477+
IntegrationHeadInjector, IntegrationHtmlContext,
477478
};
478479
use crate::streaming_processor::{Compression, PipelineConfig, StreamingPipeline};
479480
use crate::test_support::tests::create_test_settings;
@@ -544,6 +545,77 @@ mod tests {
544545
assert!(!processed.contains("remove-me"));
545546
}
546547

548+
#[test]
549+
fn integration_head_injector_prepends_after_tsjs_once() {
550+
struct TestHeadInjector;
551+
552+
impl IntegrationHeadInjector for TestHeadInjector {
553+
fn integration_id(&self) -> &'static str {
554+
"test"
555+
}
556+
557+
fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
558+
vec![r#"<script>window.__testHeadInjector=true;</script>"#.to_string()]
559+
}
560+
}
561+
562+
let html = r#"<html><head><title>Test</title></head><body></body></html>"#;
563+
564+
let mut config = create_test_config();
565+
config.integrations = IntegrationRegistry::from_rewriters_with_head_injectors(
566+
Vec::new(),
567+
Vec::new(),
568+
vec![Arc::new(TestHeadInjector)],
569+
);
570+
571+
let processor = create_html_processor(config);
572+
let pipeline_config = PipelineConfig {
573+
input_compression: Compression::None,
574+
output_compression: Compression::None,
575+
chunk_size: 8192,
576+
};
577+
let mut pipeline = StreamingPipeline::new(pipeline_config, processor);
578+
579+
let mut output = Vec::new();
580+
pipeline
581+
.process(Cursor::new(html.as_bytes()), &mut output)
582+
.expect("pipeline should process HTML");
583+
let processed = String::from_utf8(output).expect("output should be valid UTF-8");
584+
585+
let tsjs_marker = "id=\"trustedserver-js\"";
586+
let head_marker = "window.__testHeadInjector=true";
587+
588+
assert_eq!(
589+
processed.matches(tsjs_marker).count(),
590+
1,
591+
"should inject unified tsjs tag once"
592+
);
593+
assert_eq!(
594+
processed.matches(head_marker).count(),
595+
1,
596+
"should inject head snippet once"
597+
);
598+
599+
let tsjs_index = processed
600+
.find(tsjs_marker)
601+
.expect("should include unified tsjs tag");
602+
let head_index = processed
603+
.find(head_marker)
604+
.expect("should include head snippet");
605+
let title_index = processed
606+
.find("<title>")
607+
.expect("should keep existing head content");
608+
609+
assert!(
610+
tsjs_index < head_index,
611+
"should inject head snippet after tsjs tag"
612+
);
613+
assert!(
614+
head_index < title_index,
615+
"should prepend head snippet before existing head content"
616+
);
617+
}
618+
547619
#[test]
548620
fn test_create_html_processor_url_replacement() {
549621
let config = create_test_config();

crates/common/src/integrations/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ pub mod testlight;
1515
pub use registry::{
1616
AttributeRewriteAction, AttributeRewriteOutcome, IntegrationAttributeContext,
1717
IntegrationAttributeRewriter, IntegrationDocumentState, IntegrationEndpoint,
18-
IntegrationHtmlContext, IntegrationHtmlPostProcessor, IntegrationMetadata, IntegrationProxy,
19-
IntegrationRegistration, IntegrationRegistrationBuilder, IntegrationRegistry,
20-
IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction,
18+
IntegrationHeadInjector, IntegrationHtmlContext, IntegrationHtmlPostProcessor,
19+
IntegrationMetadata, IntegrationProxy, IntegrationRegistration, IntegrationRegistrationBuilder,
20+
IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction,
2121
};
2222

2323
type IntegrationBuilder = fn(&Settings) -> Option<IntegrationRegistration>;

crates/common/src/integrations/registry.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ pub struct IntegrationMetadata {
503503
pub routes: Vec<IntegrationEndpoint>,
504504
pub attribute_rewriters: usize,
505505
pub script_selectors: Vec<&'static str>,
506+
pub head_injectors: usize,
506507
}
507508

508509
impl IntegrationMetadata {
@@ -512,6 +513,7 @@ impl IntegrationMetadata {
512513
routes: Vec::new(),
513514
attribute_rewriters: 0,
514515
script_selectors: Vec::new(),
516+
head_injectors: 0,
515517
}
516518
}
517519
}
@@ -725,6 +727,13 @@ impl IntegrationRegistry {
725727
entry.script_selectors.push(rewriter.selector());
726728
}
727729

730+
for injector in &self.inner.head_injectors {
731+
let entry = map
732+
.entry(injector.integration_id())
733+
.or_insert_with(|| IntegrationMetadata::new(injector.integration_id()));
734+
entry.head_injectors += 1;
735+
}
736+
728737
map.into_values().collect()
729738
}
730739

@@ -751,7 +760,7 @@ impl IntegrationRegistry {
751760
}
752761

753762
#[cfg(test)]
754-
#[must_use]
763+
#[must_use]
755764
pub fn from_rewriters_with_head_injectors(
756765
attribute_rewriters: Vec<Arc<dyn IntegrationAttributeRewriter>>,
757766
script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,

docs/guide/creative-processing.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,32 @@ impl IntegrationScriptRewriter for NextJsIntegration {
666666
- `replace(content)` - Replace script content
667667
- `remove_node()` - Delete script element
668668

669+
### Head Injectors
670+
671+
Integrations can inject HTML snippets at the start of `<head>`, immediately after the unified TSJS bundle:
672+
673+
**Example**: An integration injects configuration that runs after the TSJS API is available
674+
675+
```rust
676+
impl IntegrationHeadInjector for MyIntegration {
677+
fn integration_id(&self) -> &'static str { "my_integration" }
678+
679+
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
680+
vec![format!(
681+
r#"<script>tsjs.setConfig({{ host: "{}" }});</script>"#,
682+
ctx.request_host
683+
)]
684+
}
685+
}
686+
```
687+
688+
**Behavior**:
689+
690+
- Snippets are prepended into `<head>` after the TSJS bundle tag
691+
- Called once per HTML response
692+
- Multiple integrations can each contribute snippets
693+
- If no snippets are returned, no extra markup is added
694+
669695
See [Integration Guide](/guide/integration-guide) for creating custom rewriters.
670696

671697
## TSJS Injection

docs/guide/integration-guide.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document explains how to integrate a new integration module with the Truste
66

77
| Component | Purpose |
88
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
9-
| `crates/common/src/integrations/registry.rs` | Defines the `IntegrationProxy`, `IntegrationAttributeRewriter`, and `IntegrationScriptRewriter` traits and hosts the `IntegrationRegistry`, which drives proxy routing and HTML/text rewrites. |
9+
| `crates/common/src/integrations/registry.rs` | Defines the `IntegrationProxy`, `IntegrationAttributeRewriter`, `IntegrationScriptRewriter`, and `IntegrationHeadInjector` traits and hosts the `IntegrationRegistry`, which drives proxy routing, HTML/text rewrites, and head injection. |
1010
| `Settings::integrations` (`crates/common/src/settings.rs`) | Free-form JSON blob keyed by integration ID. Use `IntegrationSettings::insert_config` to seed configs; each module deserializes and validates (`validator::Validate`) its own config and exposes an `enabled` flag so the core settings schema stays stable. |
1111
| Fastly entrypoint (`crates/fastly/src/main.rs`) | Instantiates the registry once per request, routes `/integrations/<id>/…` requests to the appropriate proxy, and passes the registry to the publisher origin proxy so HTML rewriting remains integration-aware. |
1212
| `html_processor.rs` | Applies first-party URL rewrites, injects the Trusted Server JS shim, and lets integrations override attribute values (for example to swap script URLs). |
@@ -79,14 +79,15 @@ pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
7979
IntegrationRegistration::builder("my_integration")
8080
.with_proxy(integration.clone())
8181
.with_attribute_rewriter(integration.clone())
82-
.with_script_rewriter(integration)
82+
.with_script_rewriter(integration.clone())
83+
.with_head_injector(integration)
8384
.with_asset("my_integration")
8485
.build(),
8586
)
8687
}
8788
```
8889

89-
Any combination of the three vectors may be populated. Modules that only need HTML rewrites can skip the `proxies` field altogether, and vice versa. The registry automatically iterates over the static builder list in `crates/common/src/integrations/mod.rs`, so adding the new `register` function is enough to make the integration discoverable.
90+
Any combination of the vectors may be populated. Modules that only need HTML rewrites can skip the `proxies` field altogether, and vice versa. The registry automatically iterates over the static builder list in `crates/common/src/integrations/mod.rs`, so adding the new `register` function is enough to make the integration discoverable.
9091

9192
### 4. Implement IntegrationProxy for Endpoints
9293

@@ -214,6 +215,29 @@ impl IntegrationScriptRewriter for MyIntegration {
214215
Returning `AttributeRewriteAction::remove_element()` (or `ScriptRewriteAction::RemoveNode` for inline content) removes the element entirely, so integrations can drop publisher-provided markup when the Trusted Server already injects a safe alternative. Prebid, for example, simply removes `prebid.js` because the unified TSJS bundle is injected automatically at the start of `<head>`.
215216
:::
216217

218+
### 5b. Implement Head Injection (Optional)
219+
220+
If the integration needs to inject HTML snippets at the start of `<head>` (for example, configuration scripts that run after the unified TSJS bundle), implement `IntegrationHeadInjector`. Snippets returned by this trait are prepended into `<head>` immediately after the TSJS bundle tag, so the `tsjs` API is available.
221+
222+
```rust
223+
impl IntegrationHeadInjector for MyIntegration {
224+
fn integration_id(&self) -> &'static str { "my_integration" }
225+
226+
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
227+
vec![format!(
228+
r#"<script>tsjs.setConfig({{ mode: "my_integration", host: "{}" }});</script>"#,
229+
ctx.request_host
230+
)]
231+
}
232+
}
233+
```
234+
235+
`html_processor.rs` calls `head_inserts` once per HTML response when the `<head>` element is first encountered. The returned snippets are concatenated after the unified script tag and prepended together, so ordering between integrations is not guaranteed — keep snippets self-contained.
236+
237+
::: tip When to Use Head Injection
238+
Use `IntegrationHeadInjector` when you need to emit configuration, inline scripts, or `<meta>` tags that must appear early in `<head>`. For attribute or script content changes on existing elements, prefer `IntegrationAttributeRewriter` or `IntegrationScriptRewriter` instead.
239+
:::
240+
217241
### 6. Register the Module
218242

219243
Add the module to `crates/common/src/integrations/mod.rs`'s builder list. The registry will call its `register` function automatically. Once registered:

docs/guide/integrations-overview.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ All integrations support:
174174

175175
### Rewriting System
176176

177-
Integrations can implement three types of rewriting:
177+
Integrations can implement four types of rewriting:
178178

179179
1. **HTTP Proxying** - Route requests through first-party domain
180180
2. **HTML Attribute Rewriting** - Modify element attributes during streaming
181181
3. **Script Content Rewriting** - Transform inline script content
182+
4. **Head Injection** - Insert HTML snippets at the start of `<head>`
182183

183184
## Choosing an Integration
184185

@@ -242,6 +243,7 @@ You can create your own integrations by implementing the integration traits:
242243
- `IntegrationProxy` - For HTTP endpoint proxying
243244
- `IntegrationAttributeRewriter` - For HTML attribute rewriting
244245
- `IntegrationScriptRewriter` - For script content transformation
246+
- `IntegrationHeadInjector` - For injecting HTML snippets into `<head>`
245247

246248
See the [Integration Guide](./integration-guide.md) for details on building custom integrations.
247249

0 commit comments

Comments
 (0)