Design · Bausteine-Dossier

Kategorie

Einbettungen & Consent

Fremde Inhalte rechtssicher einbinden — und tote Links vermeiden. Die Karte lädt erst nach aktiver Zustimmung (TDDDG/DSGVO), Social-Buttons erscheinen nur, wenn ein Profil wirklich gepflegt ist. Akzentfarbe über --akzent in einer Zeile anpassbar.

Click-to-Load-Map (Two-Click-Lösung)

Pol 2 · Klarheit Anlass: Standort-/Einsatzgebiet-Karte

Statt direktem iframe zuerst ein gestalteter Platzhalter mit Hinweistext und „Karte laden“-Knopf; erst der Klick erzeugt das <iframe loading="lazy">, die Wahl wird in localStorage gemerkt. So fließen keine Daten zu Google (USA), bevor eingewilligt wurde — Pflicht nach TDDDG (§ 25, Zugriff auf das Endgerät) und DSGVO (Drittland-Übermittlung). In dieser Demo lädt bewusst eine Attrappe (Karten-Raster statt echtem Google-iframe) — DSGVO/offline-sicher; im Projekt die echte Maps-URL einsetzen. Vorbild: Hauszeit, Nördlicht, Bernstein.

<div class="fx-map" id="fxMap">
  <div class="fx-map-consent" id="fxMapConsent">
    <svg viewBox="0 0 48 48" aria-hidden="true"><path d="M24 42s-13-12-13-22a13 13 0 0 1 26 0c0 10-13 22-13 22z"/><circle cx="24" cy="20" r="5"/></svg>
    <h4>Karte (Google Maps)</h4>
    <p>Beim Laden werden Daten an Google in den USA übermittelt. Bitte willigen Sie gemäß
      <a href="datenschutz.html">Datenschutzerklärung</a> ein.</p>
    <button type="button" class="fx-map-btn" data-map-load>Karte einmalig laden</button>
  </div>
</div>

<style>
.fx-map{position:relative;width:320px;max-width:100%;aspect-ratio:16/10;border-radius:14px;overflow:hidden;
  border:1px solid #26262c;background:#16161a}
.fx-map iframe{width:100%;height:100%;border:0;display:block}
.fx-map-consent{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:12px;text-align:center;padding:22px;
  background:repeating-linear-gradient(45deg,#17171b,#17171b 11px,#1c1c22 11px,#1c1c22 22px)}
.fx-map-consent svg{width:34px;height:34px;fill:none;stroke:var(--akzent, #7c87ff);stroke-width:1.6}
.fx-map-consent h4{margin:0;font-size:.95rem;color:#f4f4f5}
.fx-map-consent p{margin:0;max-width:280px;font-size:.82rem;color:#a1a1aa;line-height:1.5}
.fx-map-consent a{color:var(--akzent, #7c87ff);text-decoration:underline}
.fx-map-btn{font:inherit;font-size:.82rem;cursor:pointer;color:#fff;background:var(--akzent, #7c87ff);
  border:0;border-radius:8px;padding:.55rem 1.2rem;transition:filter .2s ease,transform .15s ease}
.fx-map-btn:hover,.fx-map-btn:focus-visible{filter:brightness(1.12);transform:translateY(-1px)}
@media (prefers-reduced-motion:reduce){.fx-map-btn{transition:none}.fx-map-btn:hover{transform:none}}
</style>

<script>
(function(){
  var KEY='consent-maps';
  var wrap=document.getElementById('fxMap');
  if(!wrap) return;

  // Echten iframe einsetzen (NUR nach Zustimmung aufgerufen)
  function buildMap(){
    if(wrap.querySelector('iframe')) return;
    var iframe=document.createElement('iframe');
    // Im Projekt: echte Google-Maps-URL eintragen, z. B.
    // iframe.src='https://www.google.com/maps?q=Musterstra%C3%9Fe%201%2C%20Hamburg&output=embed';
    iframe.src='https://www.google.com/maps?q=Hamburg&output=embed';
    iframe.title='Standort auf der Karte';
    iframe.loading='lazy';
    iframe.setAttribute('referrerpolicy','no-referrer-when-downgrade');
    iframe.setAttribute('allowfullscreen','');
    wrap.innerHTML='';
    wrap.appendChild(iframe);
  }

  // Klick auf „Karte laden“: Wahl merken + Karte bauen
  wrap.querySelector('[data-map-load]').addEventListener('click',function(){
    try{ localStorage.setItem(KEY,'accept'); }catch(e){}
    buildMap();
  });

  // Beim erneuten Besuch: war schon zugestimmt? -> direkt laden
  var stored=null;
  try{ stored=localStorage.getItem(KEY); }catch(e){}
  if(stored==='accept') buildMap();
})();
</script>

Auto-hide Social-Links (Config-Objekt)

Pol 2 · Klarheit Anlass: Footer / Kontakt-Social

Ein zentrales JS-Objekt setzt href + rel="noopener" nur für gepflegte URLs und blendet leere Buttons aus (display:none) — nie ein toter #-Link nach außen. In der Demo sind absichtlich nur zwei der drei Profile gepflegt: der dritte Button fehlt korrekt. Vorbild: Nördlicht.

<nav class="fx-social" aria-label="Soziale Profile">
  <a data-social="facebook"  aria-label="Facebook"><svg ...></svg></a>
  <a data-social="instagram" aria-label="Instagram"><svg ...></svg></a>
  <a data-social="linkedin"  aria-label="LinkedIn"><svg ...></svg></a>
</nav>

<style>
.fx-social{display:inline-flex;gap:.5rem}
.fx-social a{display:grid;place-items:center;width:42px;height:42px;border-radius:50%;
  color:#c9c9d2;background:#1c1c22;border:1px solid #26262c;text-decoration:none;
  transition:background .25s ease,color .25s ease,border-color .25s ease,transform .2s ease}
.fx-social a:hover,.fx-social a:focus-visible{background:var(--akzent, #7c87ff);color:#fff;
  border-color:var(--akzent, #7c87ff);transform:translateY(-2px)}
.fx-social svg{width:20px;height:20px;fill:currentColor}
@media (prefers-reduced-motion:reduce){.fx-social a{transition:none}.fx-social a:hover{transform:none}}
</style>

<script>
/* === Social-Links zentral pflegen: leer lassen = Button verschwindet === */
var SOCIAL_LINKS = {
  facebook:  'https://www.facebook.com/musterprofil',
  instagram: 'https://www.instagram.com/musterprofil',
  linkedin:  ''   // leer -> Button wird ausgeblendet, kein toter Link
};

function applySocialLinks(){
  document.querySelectorAll('[data-social]').forEach(function(el){
    var url = (SOCIAL_LINKS[el.dataset.social] || '').trim();
    if(url){
      el.href = url;
      el.target = '_blank';
      el.rel = 'noopener noreferrer';
      el.style.display = '';
    } else {
      el.style.display = 'none';   // kein gepflegtes Profil -> Button raus
    }
  });
}
document.addEventListener('DOMContentLoaded', applySocialLinks);
</script>

Click-to-Load-Video (YouTube-Platzhalter)

Pol 1 · Craft Anlass: Referenz-/Erklärvideo im Text

Ein gestaltetes Vorschaubild mit Play-Knopf steht als <button> anstelle des YouTube-<iframe>; erst der Klick erzeugt den Player über die youtube-nocookie.com-Domain und startet ihn (autoplay=1). Vorher fließen keine Daten zu Google — Pflicht nach TDDDG (§ 25) und DSGVO. Das Vorschaubild ist bewusst ein CSS-Verlauf statt des echten YouTube-Thumbnails, weil auch dessen Abruf schon eine Verbindung zu Google-Servern öffnet. In dieser Demo lädt eine Attrappe statt echtem iframe (DSGVO/offline-sicher); im Projekt die echte Video-ID einsetzen. Vorbild: Hauszeit, Nördlicht.

<div class="fx-vid" id="fxVid">
  <button type="button" class="fx-vid-poster" data-vid-load
          aria-label="Video abspielen (lädt YouTube und überträgt Daten an Google)">
    <span class="fx-vid-play" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></span>
    <span class="fx-vid-cap">
      <strong>Referenzprojekt im Video</strong>
      <span>Klick lädt YouTube · Daten an Google (USA)</span>
    </span>
  </button>
</div>

<style>
.fx-vid{position:relative;width:100%;max-width:340px;aspect-ratio:16/9;border-radius:14px;overflow:hidden;
  border:1px solid #26262c;background:#0f0f13}
.fx-vid iframe{width:100%;height:100%;border:0;display:block}
/* Vorschaubild als CSS-Verlauf (kein Thumbnail-Abruf = kein Datenabfluss) */
.fx-vid-poster{position:absolute;inset:0;border:0;padding:0;margin:0;cursor:pointer;display:block;
  background:
    radial-gradient(120% 120% at 30% 20%,rgba(124,135,255,.28),transparent 55%),
    linear-gradient(135deg,#1c1c26,#101014 70%);
  transition:filter .25s ease}
.fx-vid-poster::after{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,transparent 55%,rgba(0,0,0,.55))}
.fx-vid-play{position:absolute;top:50%;left:50%;translate:-50% -50%;z-index:2;
  width:62px;height:62px;border-radius:50%;display:grid;place-items:center;
  background:var(--akzent, #7c87ff);box-shadow:0 8px 24px -6px rgba(0,0,0,.6);
  transition:transform .25s ease,filter .2s ease}
.fx-vid-play svg{width:24px;height:24px;fill:#fff;margin-left:3px}
.fx-vid-cap{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:12px 14px;text-align:left}
.fx-vid-cap strong{display:block;font-size:.9rem;color:#f4f4f5}
.fx-vid-cap span{display:block;font-size:.74rem;color:#c9c9d2;margin-top:2px}
.fx-vid-poster:hover .fx-vid-play,.fx-vid-poster:focus-visible .fx-vid-play{transform:scale(1.08);filter:brightness(1.1)}
.fx-vid-poster:focus-visible{outline:2px solid var(--akzent, #7c87ff);outline-offset:2px}
@media (prefers-reduced-motion:reduce){
  .fx-vid-poster,.fx-vid-play{transition:none}
  .fx-vid-poster:hover .fx-vid-play,.fx-vid-poster:focus-visible .fx-vid-play{transform:none}}
</style>

<script>
(function(){
  var wrap=document.getElementById('fxVid');
  if(!wrap) return;
  var btn=wrap.querySelector('[data-vid-load]');
  if(!btn) return;

  btn.addEventListener('click',function(){
    var id='dQw4w9WgXcQ';                 // Im Projekt: echte Video-ID eintragen
    var iframe=document.createElement('iframe');
    // youtube-nocookie: setzt erst nach der Wiedergabe Cookies
    iframe.src='https://www.youtube-nocookie.com/embed/'+id+'?autoplay=1&rel=0';
    iframe.title='Video-Player';
    iframe.loading='lazy';
    iframe.setAttribute('allow','autoplay; encrypted-media; fullscreen');
    iframe.setAttribute('allowfullscreen','');
    wrap.innerHTML='';
    wrap.appendChild(iframe);
  });
})();
</script>

Social-Post-Platzhalter (Consent-Skelett)

Pol 2 · Klarheit Anlass: eingebetteter Instagram-/X-Beitrag

Statt das Embed-Skript des Netzwerks direkt zu laden (das sofort Tracking-Cookies setzt), zeigt die Seite ein neutrales Beitrags-Skelett mit Consent-Hinweis. Erst der Klick fügt das echte Embed-Skript per <script>-Injection ein. So bleibt die Seite ohne Einwilligung sauber (TDDDG § 25, DSGVO). In dieser Demo wird kein echtes Skript geladen — der Klick blendet nur einen Ersatz-Text ein (DSGVO/offline-sicher). Vorbild: Hauszeit, Bernstein.

Dieser Beitrag wird von einem sozialen Netzwerk geladen. Dabei werden Daten an den Anbieter übermittelt. Mehr in der Datenschutzerklärung.

<div class="fx-post" id="fxPost">
  <div class="fx-post-skel" data-post-skel>
    <div class="fx-post-row">
      <div class="fx-post-av"></div>
      <div class="fx-post-lines"><i></i><i></i></div>
    </div>
    <div class="fx-post-body"><i></i><i></i><i></i></div>
    <div class="fx-post-consent">
      <p>Dieser Beitrag wird von einem sozialen Netzwerk geladen. Dabei werden Daten
        an den Anbieter übermittelt. Mehr in der <a href="datenschutz.html">Datenschutzerklärung</a>.</p>
      <button type="button" class="fx-post-btn" data-post-load>Beitrag laden</button>
    </div>
  </div>
  <div class="fx-post-loaded" data-post-loaded hidden></div>
</div>

<style>
.fx-post{width:100%;max-width:320px;border-radius:14px;overflow:hidden;
  border:1px solid #26262c;background:#17171b}
.fx-post-skel{padding:16px}
.fx-post-skel[hidden]{display:none}
.fx-post-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.fx-post-av{width:38px;height:38px;border-radius:50%;flex:0 0 auto;
  background:linear-gradient(135deg,#2a2a33,#1c1c22)}
.fx-post-lines{flex:1;min-width:0}
.fx-post-lines i{display:block;height:9px;border-radius:5px;background:#26262c}
.fx-post-lines i:first-child{width:55%;margin-bottom:6px}
.fx-post-lines i:last-child{width:35%;background:#202028}
.fx-post-body i{display:block;height:9px;border-radius:5px;background:#222229;margin-bottom:7px}
.fx-post-body i:nth-child(1){width:100%}
.fx-post-body i:nth-child(2){width:88%}
.fx-post-body i:nth-child(3){width:64%}
.fx-post-consent{margin-top:14px;padding-top:14px;border-top:1px solid #222229;text-align:center}
.fx-post-consent p{margin:0 0 10px;font-size:.78rem;color:#a1a1aa;line-height:1.5}
.fx-post-consent a{color:var(--akzent, #7c87ff);text-decoration:underline}
.fx-post-btn{font:inherit;font-size:.8rem;font-weight:600;cursor:pointer;color:#fff;
  background:var(--akzent, #7c87ff);border:0;border-radius:8px;padding:.5rem 1.1rem;
  transition:filter .2s ease,transform .15s ease}
.fx-post-btn:hover,.fx-post-btn:focus-visible{filter:brightness(1.12);transform:translateY(-1px)}
.fx-post-loaded{padding:16px;font-size:.82rem;color:#c9c9d2;line-height:1.55}
.fx-post-loaded[hidden]{display:none}
@media (prefers-reduced-motion:reduce){
  .fx-post-btn{transition:none}
  .fx-post-btn:hover,.fx-post-btn:focus-visible{transform:none}}
</style>

<script>
(function(){
  var wrap=document.getElementById('fxPost');
  if(!wrap) return;
  var skel=wrap.querySelector('[data-post-skel]');
  var slot=wrap.querySelector('[data-post-loaded]');
  var btn=wrap.querySelector('[data-post-load]');
  if(!skel||!slot||!btn) return;

  btn.addEventListener('click',function(){
    // Im Projekt: hier das echte Embed-Skript des Netzwerks nachladen, z. B.
    // var s=document.createElement('script');
    // s.src='https://platform.example.com/embed.js'; s.async=true;
    // slot.appendChild(<blockquote class="…">…</blockquote>); slot.appendChild(s);
    skel.hidden=true;
    slot.hidden=false;
  });
})();
</script>