Blog

Code Snippet: Consent for External Content

Version: 1.1.2 (Feb 01, 2023)

Introduction

On many websites, external content, such as Google Maps, is embedded.

If you use the standard procedures provided in WordPress (Classic Editor, Gutenberg) or Page Builders (Oxygen, and many others) for this purpose and do not ask for the visitor's consent beforehand, you violate the data protection directives (GDPR) in the EU: When displaying a page that loads external content, a connection to an external server is automatically established. In the process, the visitor's IP address, and thus personal information, is transmitted to the external server. This is not allowed without explicit consent.

So how can you embed external content on your website in a GDPR-compliant way?

Solution

The code snippet described here provides a WordPress shortcode that allows embedding external content in a way that is compliant with the GDPR and at the same time as simple and efficient as possible.

Using this shortcode, a notice text and a consent button are displayed. The external content is only loaded after the visitor has clicked the button. This method is GDPR-compliant, because the visitor must actively confirm that he wants to load the external content.

And by the way, this solution should also have a significantly positive effect on the well-known speed test tools (Page Speed Insights, GTMetrix, …), since no data is loaded from external servers without prior confirmation.

Shortcode

The shortcode consists of an opening and a closing tag. In between, the content to be confirmed is embedded.

[ma-content-consent] external content [/ma-content-consent]

Here is an example of embedding a Google map:

[ma-content-consent] 

<iframe src="https://www.google.com/­maps/embed?pb=..." width="100%" height="300" 
style="border:0;" allowfullscreen="" loading="lazy"></iframe>

[/ma-content-consent]

The page now first displays a notice text with a link to the privacy policy. Below this, a button requests the visitor's consent to load the external content. Only after clicking the button, a connection to the external server is actually established and the actual content is loaded and displayed.

Shortcode Parameters

The code snippet implements a rather simple design for the hint text and the button. Using shortcode parameters the appearance can be influenced.

id

In the output, the HTML element receives an automatically generated, unique HTML ID according to the pattern ma-content-consent-61fbc4de4ba92. The id parameter can be used to assign your own HTML ID.

block-style

The consent block has basic CSS styles assigned by default:

display: flex; 
flex-direction: column; 
justify-content: center; 
align-content: center; 
background-color: #efefef; 
border: 1px solid lightgray; 
padding: 1em; 
width: 100%; 

The shortcode parameter block-style can be used to override these CSS styles or add more.

Sample:

[ma-content-consent block-style="height: 300px; border-radius: 5px;"] ... [/ma-content-consent]

block-class

If multiple consent blocks are to have an identical design, a CSS class can be assigned with the block-class parameter. 

Sample:

[ma-content-consent block-class="my-block"] ... [/ma-content-consent]

text

The code snippet includes a notice text about the privacy policy in different languages:

LanguageText
DANår du har trykket, vil indholdet blive hentet fra eksterne servere. Se vores privatlivspolitik for flere informationer.
DEBei Klick wird dieser Inhalt von externen Servern geladen. Details siehe Datenschutzerklärung.
ENWhen clicked, this content is loaded from external servers. See our privacy policy for details.
ESAl hacer clic, este contenido se carga desde servidores externos. Consulte la política de privacidad para más detalles.
FIKun tätä sisältöä napsautetaan, se ladataan ulkoisilta palvelimilta. Lisätietoja on tietosuojaselosteessa.
FREn cliquant, ce contenu est chargé depuis des serveurs externes. Voir la politique de confidentialité.
HUHa rákattint, ez a tartalom külső szerverekről töltődik be. A részletekért olvassa el az Adatkezelési Tájékoztatót oldalt.
ITQuando viene cliccato, questo contenuto viene caricato da server esterni. Vedere l'informativa sulla privacy per i dettagli.
JAクリックすると、以下のコンテンツが外部サーバーから読み込まれます。弊社のプライバシーポリシーの詳細は、プライバシーポリシー

The language is selected based on your website's or page language. Polylang language support is included in the snippet.
If no default text is available for your page's language, English is selected instead.

The notice text can be customized to your own requirements, for example other languages, via parameter text, and can contain the following placeholders:
{privacy-policy-url} will be replaced by the URL of the privacy policy page configured in WordPress.
{privacy-policy-link} will be replaced with a full link to the privacy policy page configured in WordPress.

Note: The predefined default texts include a link to the privacy policy, if it's properly configured in WordPress.

[ma-content-consent text="Wanneer erop geklikt wordt, wordt deze inhoud van externe servers geladen. Zie het {privacy-policy-link} voor details."] ... [/ma-content-consent]

text-style

The notice text has basic CSS styles assigned by default:

font-size: .7em;

Using the text-style shortcode parameter, these CSS styles can be overridden or additional ones can be added.

Sample:

[ma-content-consent text-style="text-shadow: 1px 1px 2px black; color: white;"] ... [/ma-content-consent]

text-class

If text in multiple consent blocks are to have an identical design, a CSS class can be assigned with the text-class parameter. 

Sample:

[ma-content-consent text-class="my-text"] ... [/ma-content-consent]

button-text

The code snippet provides the button label in different languages:

LanguageText
DAIndlæs eksternt indhold
DEExternen Inhalt laden
ENLoad external content
ESCargar contenido externo
FILataa ulkoista sisältöä
FRCharger le contenu externe
HUKülső tartalom betöltése
ITCaricare il contenuto esterno
JA外部のコンテンツを読み込ませます。

The language is selected based on your website's or page language. Polylang language support is included in the snippet.
If no default text is available for your page's language, English is selected instead.

The button label can be adapted to your own requirements, for example other languages, using the button-text parameter.

Sample:

[ma-content-consent button-text="Load Map"] ... [/ma-content-consent]

button-style

The button has no CSS styles assigned by default. It gets its design from the default WordPress button style via the CSS class wp-block-button__link.

You can use the button-style shortcode parameter to customize the button's design.

Sample:

[ma-content-consent button-style="background-color:#2a4b9b; padding:2px 10px; border-radius:5px;"] ... [/ma-content-consent]

button-class

If buttons in multiple consent blocks are to have an identical design, a CSS class can be assigned with the button-class parameter. 

Sample:

[ma-content-consent button-class="my-button"] ... [/ma-content-consent]

background-image

Optionally, a background image can be assigned to the consent block with the shortcode parameter background-image.

Sample:

[ma-content-consent background-image="https://.../wp-content/uploads/2022/02..."] ... [/ma-content-consent]

If a background image was assigned with this parameter, the following CSS styles are also set automatically:

background-size: cover; 
background-position: center center;

However, these CSS styles can be overridden with the block-styles parameter described above and thus customized as needed.

Download

The code snippet is available for download here:

ma-content-consent.json
Version 1.1.2, 2023-02-01

For installation and use of the downloaded JSON file you will need the plugin Code Snippets or Advanced Scripts.
You can install the JSON file using the "Import" function of the plugin. 
Don't forget to activate the snippet after import.

Alternative: At the end of this page you can view and copy the complete source code of the snippet.

New functionalities and bug fixes are documented in the change log.

Donation

I enjoy developing code snippets and solving requirements with them. I provide the snippets free of charge.

If you like, you can honor my many hours of work with a small coffee donation via PayPal.

  When clicking the button, a connection to PayPal is established.

Your donation will of course be taxed properly by me.

Disclaimer

I have developed and tested the code snippet to the best of my knowledge with the most current WordPress version.
I provide the code snippet for free use.
I cannot give any guarantee for the functionality because of the countless possible variations in WordPress environments.
Download and use of this code snippet is at your own risk and responsibility.

Change Log

See "Version History" in Source Code

Source Code

<?php
/*
Plugin Name:  MA Content Consent
Description:  Shortcode for requesting user consent before loading external content.
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Project:      Code Snippet: MA Content Consent
Version:      1.1.2
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-shortcode-content-consent/
Description:  en: https://www.altmann.de/en/blog-en/code-snippet-consent-for-external-content/
              de: https://www.altmann.de/blog/code-snippet-zustimmung-fuer-externe-inhalte/
Copyright:    © 2021-2023, Matthias Altmann



Version History:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
2023-02-01	1.1.2		Changes:
						- Added Finnish translations for notice text and button label
						- Changed button tag from <a> to <span> to avoid Lighthouse SEO warning "Links not crawlable"
						Fixes:
						- Support for Oxygen Repeater (due to numbered IDs)
						  (Thanks to André Slotta for reporting and supporting with code adaptions)
2022-11-15	1.1.1		Changes:
						- Added Japanese translations for notice text and button label
						  (Thanks to Viorel-Cosmin Miron)
						- Added support for WPML
						  (Thanks to Viorel-Cosmin Miron)
						  Please note there's a WPML bug preventing the creation a policy link with the correct language
						  (https://wpml.org/forums/topic/get_the_privacy_policy_link-should-be-translated/#post-8153387) 
						Fixes:
						- Support for UTF-8 content in JS base64 decoding
						  (Thanks to Tobias Przybilla for reporting)
2022-10-21	1.1.0		Changes:
						- Added new shortcode parameters block-class, text-class, button-class
						- Migrated JavaScript from jQuery to vanilla JS (ES6) to eliminate jQuery dependency.
						  (Thanks to André Slotta for the provided code sample)
						Fixes:
						- Removed double '.' for German GDPR text.
						  (Thanks to Tobias Maximilian Hietsch for reporting)
2022-02-04	1.0.1		- Added Dansk translations for notice text and button label
						  (Thanks to Theis L. Soelberg)
						- Evaluating nested shortcodes
						  (Thanks to Anja Kretzer)
2022-02-03	1.0.0		Initial release as Code Snippet
2021-12-29	0.0.1		Initial version for client project
--------------------------------------------------------------------------------------------------------------
*/

if (!class_exists('MA_Content_Consent')) :

class MA_Content_Consent {
	const TITLE							= 'MA Content Consent';
	const SLUG							= 'ma-content-consent';
	const VERSION						= '1.1.2';


	// ===== CONFIGURATION ==============================================================================================
	public static $timing				= false; 	// Write timing info to wordpress debug.log if WP_DEBUG enabled		
	public static $debug				= false; 	// Write debug info to wordpress debug.log if WP_DEBUG enabled	

	private static $default_consent_text = [ 		// consent text in different languages
		'da' => ['Når du har trykket, vil indholdet blive hentet fra eksterne servere. Se vores %s for flere informationer.','privatlivspolitik'],
		'de' => ['Bei Klick wird dieser Inhalt von externen Servern geladen. Details siehe %s.', 'Datenschutzerklärung'],
		'en' => ['When clicked, this content is loaded from external servers. See our %s for details.', 'privacy policy'],
		'es' => ['Al hacer clic, este contenido se carga desde servidores externos. Consulte la %s para más detalles.', 'política de privacidad'],
		'fi' => ['Kun tätä sisältöä napsautetaan, se ladataan ulkoisilta palvelimilta. Lisätietoja on %s.','tietosuojaselosteessa'],
		'fr' => ['En cliquant, ce contenu est chargé depuis des serveurs externes. Voir la %s.', 'politique de confidentialité'], 
		'hu' => ['Ha rákattint, ez a tartalom külső szerverekről töltődik be. A részletekért olvassa el az %s oldalt.', 'Adatkezelési Tájékoztatót'],
		'it' => ['Quando viene cliccato, questo contenuto viene caricato da server esterni. Vedere %s per i dettagli.', 'l\'informativa sulla privacy'],
		'ja' => ['クリックすると、以下のコンテンツが外部サーバーから読み込まれます。弊社のプライバシーポリシーの詳細は、%s', 'プライバシーポリシー'],
	]; 
	private static $default_button_text = [ 		// button text in different languages
		'da' => 'Indlæs eksternt indhold',
		'de' => 'Externen Inhalt laden',
		'en' => 'Load external content',
		'es' => 'Cargar contenido externo',
		'fi' => 'Lataa ulkoista sisältöä',
		'fr' => 'Charger le contenu externe', 
		'hu' => 'Külső tartalom betöltése',
		'it' => 'Caricare il contenuto esterno',
		'ja' => '外部のコンテンツを読み込ませます。',
	]; 

	//-------------------------------------------------------------------------------------------------------------------
	public static function init() {
		$st = microtime(true);

		add_shortcode('ma-content-consent', [__CLASS__, 'shortcode']);

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	private static function get_current_language(){
		$retval = get_locale();
		// Is Polylang available? 
		if (function_exists('pll_current_language')) {$retval = pll_current_language();}
		elseif (function_exists('wpml_current_language')) {$retval = wpml_current_language();}
		$retval = str_replace('_','-',$retval);
		$retval = explode('-',@$retval)[0];
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	private static function get_privacy_policy_link($text) {
		// return a link to the privacy policy (if configured) or just the passed text
		$pplink = get_the_privacy_policy_link();
		return  $pplink ? $pplink : $text; 
	}

	//-------------------------------------------------------------------------------------------------------------------
	public static function shortcode($atts, $content = '') {
		$st = microtime(true);
		$lang = self::get_current_language();
		if (!is_array($atts)) $atts = [];
		
		// fix Gutenberg content: remove </p> ... <p>
		$content = preg_replace('/^<\/p>[ \r\n]*/','',$content); // remove leading </p> tag, spaces, line breaks from shortcode start tag
		$content = preg_replace('/[ \r\n]*<p>$/','',$content); // remove trailing line breaks, spaces, <p> tag from shortcode end tag
		// evaluate nested shortcodes
		$content = do_shortcode($content);

		// get defaults for unspecified atts
		$atts = (object)array_merge([
			'slug'					=> self::SLUG,
			'id'					=> uniqid(self::SLUG . '-'),
			'block-style'			=> '',
			'block-class'			=> '',
			'text'					=> isset(self::$default_consent_text[$lang]) 
										? sprintf(self::$default_consent_text[$lang][0],self::get_privacy_policy_link(self::$default_consent_text[$lang][1])) 
										: sprintf(self::$default_consent_text['en'][0],self::get_privacy_policy_link(self::$default_consent_text['en'][1])),
			'text-style'			=> '',
			'text-class'			=> '',
			'button-text'			=> self::$default_button_text[$lang] ?? self::$default_button_text['en'],
			'button-style'			=> '',
			'button-class'			=> '',
			'background-image'		=> null,
			'contentb64'			=> base64_encode($content),
		], $atts);
		$atts->{'background-style'} = $atts->{'background-image'} ? 'background-image:url('.$atts->{'background-image'}.'); background-size:cover; background-position:center center;' : '';

		$atts->{'text'} = str_replace('{privacy-policy-url}', get_privacy_policy_url(), $atts->{'text'});
		$atts->{'text'} = str_replace('{privacy-policy-link}', get_the_privacy_policy_link(), $atts->{'text'});

		// base64 decode supporting UTF-8 from https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings#answer-30106551
		$html = <<<END_OF_HTML
		<div id="{$atts->id}" class="{$atts->slug} {$atts->{'block-class'}}" style="display:flex; flex-direction:column; justify-content:center; align-content:center; background-color:#efefef; border: 1px solid lightgray; padding:1em; width: 100%; {$atts->{'background-style'}} {$atts->{'block-style'}} ">
			<div style="text-align:center">
				<p class="{$atts->slug}__text {$atts->{'text-class'}}" style="font-size:.7em; {$atts->{'text-style'}}">{$atts->{'text'}}</p>
				<span id="{$atts->id}__button" class="wp-block-button__link {$atts->slug}__button {$atts->{'button-class'}}" style="display:inline-block; {$atts->{'button-style'}}">{$atts->{'button-text'}}</span>
			</div>
			<div id="{$atts->id}__content" class="{$atts->slug}__content" style="display:none">{$atts->contentb64}</div>
			<script>
			document.querySelector('[id^="{$atts->id}__button"]').addEventListener('click',function(){
				let consent = this.closest('div.ma-content-consent');
				let b64 = consent.querySelector('.ma-content-consent__content').innerText;
				let content = decodeURIComponent(atob(b64).split('').map(function(c) {
					return '%'+('00'+c.charCodeAt(0).toString(16)).slice(-2);
				}).join(''));
				consent.outerHTML = content;
			});
			</script>
		</div>
END_OF_HTML;

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '-> ', __CLASS__, __FUNCTION__, $et-$st));}
		return $html;
	}
}

//===================================================================================================================
// Initialize

// Warn about incompatibilities (currently none)
add_action('wp_loaded',function(){
	if (is_admin()) {
		if (!isset($GLOBALS['MA_Content_Consent_Incompatibilities'])) {$GLOBALS['MA_Content_Consent_Incompatibilities'] = [];}
		if (count($GLOBALS['MA_Content_Consent_Incompatibilities'])) {
			if (WP_DEBUG && (MA_Content_Consent::$debug || MA_Content_Consent::$timing)) 
				{error_log('MA_Content_Consent / Incompatibilities: '.print_r($GLOBALS['MA_Content_Consent_Incompatibilities'],true));}
			add_action('admin_notices', function(){
				if (WP_DEBUG ) {error_log('MA_Content_Consent / Incompatibilities: '.print_r($GLOBALS['MA_Content_Consent_Incompatibilities'],true));}
				$implementation = basename(__FILE__) == 'ma-content-consent.php' ? 'Plugin' : 'Code Snippet';
				echo '<div class="notice notice-warning xis-dismissible">
						<p>The '.$implementation.'  "MA Content Consent" is skipped: '.implode(' or ',$GLOBALS['MA_Content_Consent_Incompatibilities']).'</p>
					</div>';
			});
		}
	}
}, 1000); 

add_action('wp_loaded',function(){
	if (count($GLOBALS['MA_Content_Consent_Incompatibilities']??[])) return;
	if (wp_doing_ajax()) 		return; 	// don't run for AJAX requests
	if (wp_doing_cron()) 		return; 	// don't run for CRON requests
	if (wp_is_json_request()) 	return; 	// don't run for JSON requests
	if (is_favicon()) 			return; 	// don't run for favicon request
	if (($_SERVER['QUERY_STRING']??'') == 'service-worker')			return;	// don't run for service-worker
	if (($_SERVER['REQUEST_URI']??'') == '/favicon.ico')			return;	// don't run for favicon
	if ((strpos($_SERVER['REQUEST_URI']??'','/wp-content/') === 0))	return;	// don't run for dynamic wp-content file
	
	if (is_admin()) {
		global $pagenow;
		if (	($pagenow != 'post-new.php') 
		&& 	(	($pagenow != 'post.php') || ($pagenow == 'post.php' && (($_REQUEST['action']??null) != 'edit'))	) 
		) return; // only load on specific requests where Gutenberg is involved
	}
		
	$implementation = basename(__FILE__) == 'ma-content-consent.php' ? 'Plugin' : 'Code Snippet';
	if (WP_DEBUG && (MA_Content_Consent::$debug || MA_Content_Consent::$timing)) 
		{error_log(sprintf('MA_Content_Consent: Initializing %s for request URI="%s" action="%s"', $implementation, $_SERVER['REQUEST_URI']??'', $_REQUEST['action']??''));}

	MA_Content_Consent::init();

}, 1200); 
	
endif;
First published: Feb 03, 2022 on Code Snippet: Consent for External Content
magnifier