Installation
Requirements
- Craft CMS 5.3 or later
- PHP 8.2 or later
That is the whole list. Bed has no runtime dependencies beyond Craft's own, no build step, and makes no outbound HTTP request anywhere — not at install, not on a render, not ever. The provider registry is a description of what embeds look like, not a client for talking to anybody.
Bed is free. There are no editions, no licence key and no licensing code in the plugin.
Install
composer require justinholtweb/craft-bed
php craft plugin/install bed
Or find Bed in the Plugin Store and install it there.
Installing creates two tables — bed_slots and bed_metrics — and nothing else. No project config
is written until you change a setting.
What happens straight away
Nothing you have to do. From the next front-end page view:
- Every embed is wrapped in a box that holds its space. Embeds whose shape is knowable — a YouTube
frame, or anything carrying
widthandheight— get anaspect-ratioimmediately and need no measurement at all. - Frames below the first one get
loading="lazy"; video and audio getpreload="none". - Script-driven embeds — a tweet, a TikTok, an Instagram post — have their loader
<script>lifted out of the document and injected by an IntersectionObserver as the embed nears the viewport. preconnectanddns-prefetchlinks are added for the providers on the page, capped at six.- YouTube embeds get a poster-and-play-button facade, swapped for the real player on the click.
- Frames with no
titleget one, and frames with no referrer policy getstrict-origin-when-cross-origin.
A page with no embeds on it is rejected by a single pattern match before any of that runs.
Then it gets better on its own
The heights Bed cannot know in advance — a tweet is as tall as the tweet — are measured, not guessed. That takes a little while to warm up:
- A page renders. Each embed gets a slot key, and the page carries a token, signed with Craft's own security key, listing exactly which slots are on it.
- One page view in ten measures what the embeds settled to and beacons it back.
- Once a slot has twenty samples at a viewport bucket, Bed emits a
min-heightfor that width and stops asking for more.
So the reserved heights appear over the first few hundred views of a page, not on the first one. Nothing is broken in the meantime — those embeds simply have no reserved height yet, which is exactly where they started.
Watch it happen:
php craft bed/metrics/report
Verifying it works
View source on a page with an embed on it. You are looking for a wrapper like this:
<div class="bed bed--vimeo bed--iframe bed--ratio" data-bed="e87d290a…" style="--bed-ar:640/360">
<iframe src="https://player.vimeo.com/video/76979871" width="640" height="360"
loading="lazy" title="Vimeo embed" referrerpolicy="strict-origin-when-cross-origin"></iframe>
</div>
If you see the original <iframe> unwrapped, check in this order:
- Enabled and Optimise front-end pages automatically are both on in the settings.
- The page's URI is not matched by Never optimise these URIs.
- Neither the embed nor anything containing it carries
data-bed-skip. - The response is an HTML front-end response with a status below 400 — Bed leaves control panel requests, JSON and error pages alone.
Configuring it in a file
Like any Craft plugin, Bed reads config/bed.php, and anything set there overrides the control
panel and cannot be changed from it:
<?php
return [
'sampleRate' => 1.0, // measure every view in dev, one in ten in production
'facadeProviders' => ['youtube'],
'excludeUris' => ['checkout/*'],
];
Multi-environment arrays work as they do everywhere else in Craft.
Next
- Configuration — every setting, and what it defaults to
- Usage — Twig, opting out, styling, the console