Skip to content

Commit 80cf16f

Browse files
committed
Using templates to manage scripts
1 parent 67c37cb commit 80cf16f

6 files changed

Lines changed: 149 additions & 204 deletions

File tree

README.md

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -255,38 +255,58 @@ var orejimeConfig = {
255255

256256
### Third-party scripts configuration
257257

258-
For each third-party script you want Orejime to manage, you must modify its `<script>` tag so that the browser doesn't load it anymore. Orejime will take care of loading it when the user consents to it.
258+
Scripts that require user consent must not be executed when the page load.
259+
Orejime will take care of loading them when the user has consented.
259260

260-
On inline scripts:
261-
* set the `type` attribute to `orejime` to keep the browser from executing the script
262-
* add a `data-purpose` containing the id of a purpose you configured previously
261+
Those scripts must be tagged with their related purpose from the configuration. This is done by wrapping them with a template tag and a `data-purpose` attribute:
263262

264263
```diff
265-
- <script>
266-
+ <script
267-
+ type="orejime"
268-
+ data-purpose="google-tag-manager"
269-
+ >
270-
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push [...]
271-
</script>
264+
+ <template data-purpose="google-tag-manager">
265+
<script>
266+
(function(w,d,s,l,i){/* … */})(window,document,'script','dataLayer','GTM-XXXX')
267+
</script>
268+
+ </template>
272269
```
273270

274-
> [!WARNING]
275-
> The `data-purpose` attribute must match the id of a purpose in the configuration.
276-
> Orejime uses this id to find a purpose's associated scripts.
277-
> If those don't match, Orejime won't be able to load the scripts.
271+
This way, the original script is left untouched, and any piece of HTML can be controlled by Orejime in the same way.
278272

279-
On external scripts or `img` tags (i.e. tracking pixels), follow the same steps and rename the `src` attribute to `data-src`:
273+
You can wrap many elements at once or use several templates with the same purpose:
274+
275+
```html
276+
<template data-purpose="ads">
277+
<script src="https://annoying-ads.net"></script>
278+
<script src="https://intrusive-advertising.io"></script>
279+
</template>
280+
281+
<template data-purpose="ads">
282+
<iframe src="https://streaming.ads-24-7.com/orejime"></iframe>
283+
</template>
284+
```
285+
286+
<details>
287+
<summary>Integration tips</summary>
288+
289+
#### WordPress
290+
291+
Should you use Orejime in a WordPress website, you could alter the rendering of the script tags it should handle:
292+
293+
```php
294+
// Register a script somewhere…
295+
wp_enqueue_script('matomo', 'matomo.js');
296+
297+
// …and change the script output to wrap it in a template.
298+
function orejimeScriptLoader($tag, $handle, $src) {
299+
if ($handle === 'matomo') {
300+
return '<template data-purpose="analytics">' + $tag + '</template>';
301+
}
302+
303+
return $tag;
304+
}
305+
306+
add_filter('script_loader_tag', 'orejimeScriptLoader', 10, 3);
280307

281-
```diff
282-
- <script
283-
- src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
284-
+ <script
285-
+ type="orejime"
286-
+ data-purpose="google-maps"
287-
+ data-src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
288-
></script>
289308
```
309+
</details>
290310

291311
### Initialization
292312

@@ -399,8 +419,29 @@ orejime.manager.on('dirty', function(isDirty) {
399419

400420
A major overhaul of the configuration took place in this version, as to clarify naming and align more with the GDPR vocabulary.
401421

422+
#### Configuration
423+
402424
If you were already using version 2, a tool to migrate your current configuration is available here : https://orejime.boscop.fr/#migration.
403425

426+
#### Third-party scripts
427+
428+
Previous versions of Orejime required you to alter third party script tags.
429+
This behavior has changed, and you should now leave scripts untouched and wrap them in a template, as documented in [scripts configuration](#third-party-scripts-configuration) ([learn why](./adr/003-purpose-templates.md)).
430+
431+
As you can see from the following example, this is simpler and less intrusive:
432+
433+
```diff
434+
- <script
435+
- type="opt-in"
436+
- data-type="application/javascript"
437+
- data-name="google-maps"
438+
- data-src="https://maps.googleapis.com/maps/api/js"
439+
- ></script>
440+
+ <template data-purpose="google-maps">
441+
+ <script src="https://maps.googleapis.com/maps/api/js"></script>
442+
+ </template>
443+
```
444+
404445
## Development
405446

406447
If you want to contribute to Orejime, or make a custom build for yourself, clone the project and run these commands:

adr/003-purpose-templates.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
date: 2025-02-08
3+
status: Accepted
4+
---
5+
6+
# Purpose templates
7+
8+
## Context
9+
10+
The current way of setting up third-party scripts is kind of confusing and requires shaky machanics to enable or disable them.
11+
There might be a leaner way to handle this.
12+
13+
## Considerations
14+
15+
* Having to modify script attributes is tedious, and can be complicated within some environments.
16+
* We're relying on hacky mechanics, namely the `type="orejime"` attribute. This makes the implementation in user land hard to explain.
17+
* The current implementation relies on data attributes to "backup" actual attributes when disabling a script, and tag removal and reinsertion when enabling it. This leads to all sort of edge cases that are hard to pinpoint.
18+
* The implementation varies depending on the HTML element that must be toggled (scripts are a special case).
19+
20+
## Decision
21+
22+
Instead of modifying elements, we'll wrap them inside `template` tags.
23+
This way :
24+
* The original script or element is left untouched.
25+
* This is a native and straighforward functionality.
26+
* One tag and attribute makes for less syntactic bloat than the previous prefix system.
27+
* With the same amount of code, a single purpose can act on one or many HTML elements.

e2e/OrejimePage.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ export class OrejimePage {
102102
await this.page.keyboard.press('Escape');
103103
}
104104

105+
// @see https://stackoverflow.com/a/73214414
106+
async expectElement(selector: string) {
107+
expect(await this.page.locator(selector)).toHaveCount(1);
108+
}
109+
110+
// @see https://stackoverflow.com/a/73214414
111+
async expectMissingElement(selector: string) {
112+
expect(await this.page.locator(selector).count()).toEqual(0);
113+
}
114+
105115
async expectConsents(consents: Record<string, unknown>) {
106116
expect(await this.getConsentsFromCookies()).toEqual(consents);
107117
}
@@ -112,17 +122,4 @@ export class OrejimePage {
112122
const {value} = cookies.find((cookie) => cookie.name === name)!;
113123
return JSON.parse(Cookie.converter.read(value, name));
114124
}
115-
116-
async expectScriptAttributes(
117-
purposeId: string,
118-
attributes: Record<string, string>
119-
) {
120-
const script = await this.page.locator(
121-
`script[data-purpose="${purposeId}"]`
122-
);
123-
124-
for (const k in attributes) {
125-
expect(script).toHaveAttribute(k, attributes[k]);
126-
}
127-
}
128125
}

e2e/orejime.spec.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@ test.describe('Orejime', () => {
3232
};
3333

3434
const BaseScripts = `
35-
<script type="orejime" data-purpose="mandatory"></script>
36-
<script type="orejime" data-purpose="child-1" data-type="application/json"></script>
35+
<template data-purpose="mandatory">
36+
<script id="mandatory"></script>
37+
</template>
38+
39+
<template data-purpose="child-1">
40+
<iframe id="child-1"></iframe>
41+
</template>
3742
`;
3843

3944
let orejimePage: OrejimePage;
@@ -61,15 +66,8 @@ test.describe('Orejime', () => {
6166
'child-2': true
6267
});
6368

64-
orejimePage.expectScriptAttributes('mandatory', {
65-
'data-purpose': 'mandatory',
66-
type: 'text/javascript'
67-
});
68-
69-
orejimePage.expectScriptAttributes('child-1', {
70-
'data-purpose': 'child-1',
71-
type: 'application/json'
72-
});
69+
orejimePage.expectElement('#mandatory');
70+
orejimePage.expectElement('#child-1');
7371
});
7472

7573
test('should decline all purposes from the banner', async () => {
@@ -82,16 +80,8 @@ test.describe('Orejime', () => {
8280
'child-2': false
8381
});
8482

85-
orejimePage.expectScriptAttributes('mandatory', {
86-
'data-purpose': 'mandatory',
87-
type: 'text/javascript'
88-
});
89-
90-
orejimePage.expectScriptAttributes('child-1', {
91-
'data-purpose': 'child-1',
92-
'data-type': 'application/json',
93-
'type': 'orejime'
94-
});
83+
orejimePage.expectElement('#mandatory');
84+
orejimePage.expectMissingElement('#child-1');
9585
});
9686

9787
test('should open a modal', async () => {

src/core/utils/updatePurposeElements.test.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,25 @@ import updatePurposeElements from './updatePurposeElements';
22

33
test('updatePurposeElements', () => {
44
document.body.innerHTML = `
5-
<script id="foo" type="orejime" data-purpose="foo" data-src="src" crossorigin="anonymous"></script>
5+
<template data-purpose="foo">
6+
<script id="foo" src="src" crossorigin="anonymous"></script>
7+
</template>
68
`;
79

810
updatePurposeElements('foo', false);
9-
const foo = document.getElementById('foo')!;
10-
11-
expect(foo.getAttribute('type')).toEqual('orejime');
12-
expect(foo.hasAttribute('src')).toBeFalsy();
13-
expect(foo.hasAttribute('data-type')).toBeFalsy();
14-
expect(foo.getAttribute('data-src')).toEqual('src');
15-
expect(foo.getAttribute('crossorigin')).toEqual('anonymous');
11+
expect(document.getElementById('foo')).toBeNull();
1612

1713
updatePurposeElements('foo', true);
1814
const foo2 = document.getElementById('foo')!;
1915

20-
expect(foo2.hasAttribute('data-type')).toBeFalsy();
21-
expect(foo2.hasAttribute('data-src')).toBeFalsy();
22-
expect(foo2.getAttribute('type')).toEqual('text/javascript');
16+
expect(foo2.getAttribute('id')).toEqual('foo');
2317
expect(foo2.getAttribute('src')).toEqual('src');
2418
expect(foo2.getAttribute('crossorigin')).toEqual('anonymous');
2519

2620
updatePurposeElements('foo', true);
27-
const foo3 = document.getElementById('foo')!;
28-
29-
expect(foo3.hasAttribute('data-type')).toBeFalsy();
30-
expect(foo3.hasAttribute('data-src')).toBeFalsy();
31-
expect(foo3.getAttribute('type')).toEqual('text/javascript');
32-
expect(foo3.getAttribute('src')).toEqual('src');
33-
expect(foo3.getAttribute('crossorigin')).toEqual('anonymous');
21+
updatePurposeElements('foo', true);
22+
expect(document.querySelectorAll('script')).toHaveLength(1);
3423

3524
updatePurposeElements('foo', false);
36-
const foo4 = document.getElementById('foo')!;
37-
38-
expect(foo4.getAttribute('type')).toEqual('orejime');
39-
expect(foo4.hasAttribute('src')).toBeFalsy();
40-
expect(foo4.getAttribute('data-type')).toEqual('text/javascript');
41-
expect(foo4.getAttribute('data-src')).toEqual('src');
42-
expect(foo4.getAttribute('crossorigin')).toEqual('anonymous');
25+
expect(document.getElementById('foo')).toBeNull();
4326
});

0 commit comments

Comments
 (0)