Browse documentation

Media that waits for permission

On this page 5 sections

Use createEmbedService for iframes and images that need an optional service's effective permission. It keeps resource URLs out of live resource attributes until every declared purpose is allowed. One service group manages several placeholders using the existing consent controller, including required-receipt gating.

The adapter is cooperative integration code. It does not classify a vendor, determine legal exemptions, intercept unrelated requests, or undo data already sent.

Declare the service's destinations

Add resourceOrigins to the service in your site configuration:

{
  id: 'optional-video',
  name: 'Video player',
  provider: 'Your selected provider',
  location: 'Your reviewed processing locations',
  purposeIds: ['embedded-media'],
  resourceOrigins: ['https://media.example.test'],
}

Also declare the purpose and service in config.consent. Origins must be exact canonical HTTPS origins, with no paths, trailing slash, credentials or wildcards. Use 'self' for a first-party resource. The public projection retains this declaration, so changing it changes the affected consent scope and invalidates older grants. Update the service scope and reviewed disclosures whenever a vendor or its processing changes, even if its origin stays the same.

Origins restrict the initial requested URL. They do not constrain redirects, frame navigation, nested resources or a provider's server processing. Configure the host's frame-src, img-src and other relevant CSP directives for the destinations you intend to permit; review vendor behavior separately.

Mount one group, then its media

import {createConsent, createEmbedService, mountControls} from 'notabenda';
import publicConfig from './legal/notabenda.public.json';

const consent = createConsent(publicConfig);
mountControls(document.querySelector('#privacy-controls'), {consent});

// One registration for this service, owned by the host bootstrap.
const videos = createEmbedService({consent, serviceId: 'optional-video'});
const player = videos.mount(document.querySelector('#video'), {
  kind: 'iframe',
  src: 'https://media.example.test/player',
  title: 'A tour of the project',
  description: 'This player uses the optional media service. Review your choices before loading it.',
  sandbox: ['allow-scripts'], // Explicitly review what the provider needs.
});

// Route/unmount cleanup: remove this surface, retain the host's service group.
player.destroy();
// Eventual application cleanup; the group cannot be registered again on this controller.
videos.destroy();
consent.destroy();

mount(container, options) uses a dedicated container and returns {refresh, destroy}. It temporarily replaces the container's existing static fallback and restores it on destroy if the container is otherwise empty. Mount one resource per container. Several mounts can share one group; destroying one does not stop the others. Service IDs remain reserved for the controller's lifetime, so do not create/destroy groups in repeatable React effects or Astro page scripts. Keep the group in the host bootstrap and manage individual mounts with route/component lifetimes.

For images use kind: 'image', a nonempty alt, and the same src, title and description fields. Sources accept HTTPS or root-relative paths, and must match the declared origins. URLs are resolved against the document URL, independent of a <base> element. Options are copied when mounting; changing the caller's object later cannot replace the destination or expand iframe permissions.

The placeholder button opens the existing preferences UI. It makes no consent choice, including no one-time grant. Keep mountControls or an equivalent working preferences UI available. Permission may already have been saved; the group still checks current effective permission before adding media, and required mode waits for acknowledged evidence.

Inert server rendering

import {renderEmbedPlaceholder} from 'notabenda';
const fallback = renderEmbedPlaceholder({
  title: 'A tour of the project',
  description: 'Optional media stays unloaded until you allow it.',
});
// Render fallback inside the dedicated #video container in your host template.

This renderer accepts only plain copy. It escapes that copy and emits no resource URL, iframe, image, resource hint or script. Its action stays hidden/disabled until the browser mount supplies a functioning action. Without JavaScript, the explanatory text remains readable and no media loads.

Do not place a live src, srcset, CSS background, remote poster, preload, preconnect or vendor script in the fallback. A detached image can initiate a request when its source is assigned; an element's being invisible or detached is not a consent gate. The adapter assigns no resource source while blocked, including on detached nodes.

Lifecycle and browser limits

Import notabenda/styles.css for optional responsive card/media styles, or replace the low-specificity .nb-embed* rules. Existing theme tokens control surfaces, text, borders and actions. Native iframe/image titles and alternatives come from the supplied plain text.

Try the example

Run npm run dev and open the fictional media example. It uses only two first-party sample resources: a script-free framed page and a generated illustration. The visible counter reports resource entries observed by the browser on this page, including requests served from cache; it is not an audit log or a claim about a live vendor.

REQ-PRODUCT-EMBEDS links the implementation and tests in the catalog, with a browser verification record. Regional device-access requirements remain partial; the adapter supplies a mechanism for the processing you have declared, not legal applicability rules.