lullabot/amp
Lullabot AMP is a PHP library that converts standard HTML into valid Accelerated Mobile Pages markup. It handles AMP-specific transformations and validation, helping PHP and Drupal projects serve faster, compliant mobile pages.
lullabot/amp converts an HTML string into AMP HTML and reports validation problems against a bundled snapshot of the AMP spec. It is a plain PHP library (no framework coupling), so in Laravel you install it and wrap it in your own service.
composer require lullabot/amp
The package pulls in masterminds/html5 and querypath/querypath; make sure ext-dom, ext-mbstring and ext-libxml are enabled (they normally are in a standard Laravel PHP image).
The entire public surface lives on Lullabot\AMP\AMP. A first conversion is three calls:
use Lullabot\AMP\AMP;
$amp = new AMP();
$amp->loadHtml($html); // full document: <html><head><body>
$amp->convertToAmpHtml();
$ampHtml = $amp->getAmpHtml();
if ($amp->getInputOutputHtmlValidationErrors()) {
// inspect error objects (code, line, column, spec_url, severity)
}
Look first at src/AMP.php for the entry points, then at src/Pass/ — every transformation (AmpImgTagTransformPass, AmpIframeTagTransformPass, YouTube/Instagram/Twitter embeds, form fixes, etc.) is a discrete pass, which is the best map of what the library will and won't do to your markup. The first realistic use case is a CMS-style blog post: render the existing Blade view, feed the full document in, store the resulting AMP HTML string, and serve it from a separate /amp/... route.
Wrap it in a stateless service. The AMP object is mutable and does not reset cleanly between documents, so never register it as a singleton. Bind a factory or bind() (not singleton()) and return a fresh helper per conversion:
class AmpConverter
{
public function convert(string $html, array $options = []): AmpResult
{
$amp = new AMP();
$amp->setOptions($options);
$amp->loadHtml($html);
$amp->convertToAmpHtml();
return new AmpResult(
$amp->getAmpHtml(),
$amp->getWarnings(),
$amp->getInputOutputHtmlValidationErrors(),
$amp->getActionTakenLines(),
);
}
}
Convert after rendering, not inside Blade. The library needs the final <html>/<head>/<body> document (or a body fragment in body scope). Render the view with view()->render(), then convert. For fragment-level conversion of a single article body, pass ['scope' => Scope::BODY_SCOPE] and embed the result into your own AMP shell (see remove_amp_script below).
Do it on a queue, not in the request. DOM parsing plus validation is the slow part; cache the AMP output keyed by hash('xxh128', $html . serialize($options)) and regenerate on save. This also gives you a natural place to log getActionTakenLines() for auditing what was rewritten.
Use the pass pipeline as an extension point. Subclass Lullabot\AMP\Pass\BasePass when you need domain-specific fixes (e.g. rewriting relative image URLs to absolute CDN URLs before the amp-img pass runs). Ordering matters — register custom passes after the standard fix passes so you operate on already-normalised markup, and inspect AMP::$passes in the current release for the exact hook before relying on it; it's an internal, not a documented, API.
Tighten the loop in CI. Keep a directory of representative HTML fixtures and assert on getInputOutputHtmlValidationErrors() (should be empty) plus a snapshot of getWarnings(). Error objects expose getSpecUrl(), so you can render AMP-doc links straight into your QA report.
Options worth knowing: scope (Scope::HTML_SCOPE / Scope::BODY_SCOPE), remove_amp_script (set true when injecting into a page that already loads v0.js), and amp_json_custom_data for injecting your own JSON-LD alongside the generated AMP boilerplate.
HTML_SCOPE yields a malformed or over-eagerly-fixed document. Either give it a full page or set BODY_SCOPE deliberately.src/href values on images, iframes and links survive into the output and trigger validator errors. Run a pass or a pre-step (Laravel url()/asset()) before conversion.amp-img needs dimensions. If the original <img> has no width/height, the converter can't invent them — you get warnings and invalid markup. Enforce dimensions in your CMS/editor, or add them in a custom pass.<script> tags, or JS-driven widgets will break. The pass list will rewrite known embeds (YouTube, Vimeo, Twitter, Instagram, <video>, <audio>, <iframe>), but custom widgets need a real amp-* component or must be excluded.style attributes and non-allowlisted <style> content are flagged or stripped; AMP only permits a single <style amp-custom> block in <head>. Expect to restructure your CSS for the AMP output.amphtml-validator in CI rather than trusting getInputOutputHtmlValidationErrors() alone.getWarnings() returns validation-warning objects, not strings — use the code/severity getters to filter, otherwise benign notices drown out real problems.loadHtml($html, $encoding) — pass the encoding explicitly if your source isn't UTF-8; otherwise non-ASCII output can be mangled.convertToAmpHtml() twice on the same object double-transforms. Build a fresh AMP instance per request/record, and use getInputHtml() to diff input vs. output while debugging.dumpAllActionsTaken() prints every transformation with a human-readable description and line reference — the fastest way to answer "why did my markup change?"How can I help you explore Laravel packages today?