Skip to content

Commit

Permalink
Using templates to manage scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
felixgirault committed Feb 10, 2025
1 parent f2751b5 commit 8994140
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 204 deletions.
89 changes: 65 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,38 +255,58 @@ var orejimeConfig = {

### Third-party scripts configuration

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.
Scripts that require user consent must not be executed when the page load.
Orejime will take care of loading them when the user has consented.

On inline scripts:
* set the `type` attribute to `orejime` to keep the browser from executing the script
* add a `data-purpose` containing the id of a purpose you configured previously
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:

```diff
- <script>
+ <script
+ type="orejime"
+ data-purpose="google-tag-manager"
+ >
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push [...]
</script>
+ <template data-purpose="google-tag-manager">
<script>
(function(w,d,s,l,i){/* … */})(window,document,'script','dataLayer','GTM-XXXX')
</script>
+ </template>
```

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

On external scripts or `img` tags (i.e. tracking pixels), follow the same steps and rename the `src` attribute to `data-src`:
You can wrap many elements at once or use several templates with the same purpose:

```html
<template data-purpose="ads">
<script src="https://annoying-ads.net"></script>
<script src="https://intrusive-advertising.io"></script>
</template>

<template data-purpose="ads">
<iframe src="https://streaming.ads-24-7.com/orejime"></iframe>
</template>
```

<details>
<summary>Integration tips</summary>

#### WordPress

Should you use Orejime in a WordPress website, you could alter the rendering of the script tags it should handle:

```php
// Register a script somewhere…
wp_enqueue_script('matomo', 'matomo.js');

// …and change the script output to wrap it in a template.
function orejimeScriptLoader($tag, $handle, $src) {
if ($handle === 'matomo') {
return '<template data-purpose="analytics">' + $tag + '</template>';
}

return $tag;
}

add_filter('script_loader_tag', 'orejimeScriptLoader', 10, 3);

```diff
- <script
- src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
+ <script
+ type="orejime"
+ data-purpose="google-maps"
+ data-src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
></script>
```
</details>

### Initialization

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

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

#### Configuration

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

#### Third-party scripts

Previous versions of Orejime required you to alter third party script tags.
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)).

As you can see from the following example, this is simpler and less intrusive:

```diff
- <script
- type="opt-in"
- data-type="application/javascript"
- data-name="google-maps"
- data-src="https://maps.googleapis.com/maps/api/js"
- ></script>
+ <template data-purpose="google-maps">
+ <script src="https://maps.googleapis.com/maps/api/js"></script>
+ </template>
```

## Development

If you want to contribute to Orejime, or make a custom build for yourself, clone the project and run these commands:
Expand Down
27 changes: 27 additions & 0 deletions adr/003-purpose-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
date: 2025-02-08
status: Accepted
---

# Purpose templates

## Context

The current way of setting up third-party scripts is kind of confusing and requires shaky machanics to enable or disable them.
There might be a leaner way to handle this.

## Considerations

* Having to modify script attributes is tedious, and can be complicated within some environments.
* We're relying on hacky mechanics, namely the `type="orejime"` attribute. This makes the implementation in user land hard to explain.
* 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.
* The implementation varies depending on the HTML element that must be toggled (scripts are a special case).

## Decision

Instead of modifying elements, we'll wrap them inside `template` tags.
This way :
* The original script or element is left untouched.
* This is a native and straighforward functionality.
* One tag and attribute makes for less syntactic bloat than the previous prefix system.
* With the same amount of code, a single purpose can act on one or many HTML elements.
23 changes: 10 additions & 13 deletions e2e/OrejimePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ export class OrejimePage {
await this.page.keyboard.press('Escape');
}

// @see https://stackoverflow.com/a/73214414
async expectElement(selector: string) {
expect(await this.page.locator(selector)).toHaveCount(1);
}

// @see https://stackoverflow.com/a/73214414
async expectMissingElement(selector: string) {
expect(await this.page.locator(selector).count()).toEqual(0);
}

async expectConsents(consents: Record<string, unknown>) {
expect(await this.getConsentsFromCookies()).toEqual(consents);
}
Expand All @@ -112,17 +122,4 @@ export class OrejimePage {
const {value} = cookies.find((cookie) => cookie.name === name)!;
return JSON.parse(Cookie.converter.read(value, name));
}

async expectScriptAttributes(
purposeId: string,
attributes: Record<string, string>
) {
const script = await this.page.locator(
`script[data-purpose="${purposeId}"]`
);

for (const k in attributes) {
expect(script).toHaveAttribute(k, attributes[k]);
}
}
}
32 changes: 11 additions & 21 deletions e2e/orejime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ test.describe('Orejime', () => {
};

const BaseScripts = `
<script type="orejime" data-purpose="mandatory"></script>
<script type="orejime" data-purpose="child-1" data-type="application/json"></script>
<template data-purpose="mandatory">
<script id="mandatory"></script>
</template>
<template data-purpose="child-1">
<iframe id="child-1"></iframe>
</template>
`;

let orejimePage: OrejimePage;
Expand Down Expand Up @@ -61,15 +66,8 @@ test.describe('Orejime', () => {
'child-2': true
});

orejimePage.expectScriptAttributes('mandatory', {
'data-purpose': 'mandatory',
type: 'text/javascript'
});

orejimePage.expectScriptAttributes('child-1', {
'data-purpose': 'child-1',
type: 'application/json'
});
orejimePage.expectElement('#mandatory');
orejimePage.expectElement('#child-1');
});

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

orejimePage.expectScriptAttributes('mandatory', {
'data-purpose': 'mandatory',
type: 'text/javascript'
});

orejimePage.expectScriptAttributes('child-1', {
'data-purpose': 'child-1',
'data-type': 'application/json',
'type': 'orejime'
});
orejimePage.expectElement('#mandatory');
orejimePage.expectMissingElement('#child-1');
});

test('should open a modal', async () => {
Expand Down
33 changes: 8 additions & 25 deletions src/core/utils/updatePurposeElements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,25 @@ import updatePurposeElements from './updatePurposeElements';

test('updatePurposeElements', () => {
document.body.innerHTML = `
<script id="foo" type="orejime" data-purpose="foo" data-src="src" crossorigin="anonymous"></script>
<template data-purpose="foo">
<script id="foo" src="src" crossorigin="anonymous"></script>
</template>
`;

updatePurposeElements('foo', false);
const foo = document.getElementById('foo')!;

expect(foo.getAttribute('type')).toEqual('orejime');
expect(foo.hasAttribute('src')).toBeFalsy();
expect(foo.hasAttribute('data-type')).toBeFalsy();
expect(foo.getAttribute('data-src')).toEqual('src');
expect(foo.getAttribute('crossorigin')).toEqual('anonymous');
expect(document.getElementById('foo')).toBeNull();

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

expect(foo2.hasAttribute('data-type')).toBeFalsy();
expect(foo2.hasAttribute('data-src')).toBeFalsy();
expect(foo2.getAttribute('type')).toEqual('text/javascript');
expect(foo2.getAttribute('id')).toEqual('foo');
expect(foo2.getAttribute('src')).toEqual('src');
expect(foo2.getAttribute('crossorigin')).toEqual('anonymous');

updatePurposeElements('foo', true);
const foo3 = document.getElementById('foo')!;

expect(foo3.hasAttribute('data-type')).toBeFalsy();
expect(foo3.hasAttribute('data-src')).toBeFalsy();
expect(foo3.getAttribute('type')).toEqual('text/javascript');
expect(foo3.getAttribute('src')).toEqual('src');
expect(foo3.getAttribute('crossorigin')).toEqual('anonymous');
updatePurposeElements('foo', true);
expect(document.querySelectorAll('script')).toHaveLength(1);

updatePurposeElements('foo', false);
const foo4 = document.getElementById('foo')!;

expect(foo4.getAttribute('type')).toEqual('orejime');
expect(foo4.hasAttribute('src')).toBeFalsy();
expect(foo4.getAttribute('data-type')).toEqual('text/javascript');
expect(foo4.getAttribute('data-src')).toEqual('src');
expect(foo4.getAttribute('crossorigin')).toEqual('anonymous');
expect(document.getElementById('foo')).toBeNull();
});
Loading

0 comments on commit 8994140

Please sign in to comment.