Fixing images on Laravel Vapor with League\Commonmark
July 20, 2021
If you are using The PHP League's terrific Commonmark markdown parsing library and hosting your site on Laravel Vapor, then you may be running into an issue where your images are not showing up.
Vapor's documentation states that you must use the asset
helper to generate the correct URLs for your images:
Because all of your assets will be served via S3 / CloudFront, you should always generate URLs to these assets using Laravel's asset helper. Vapor injects an
ASSET_URL
environment variable which Laravel's asset helper will use when constructing your URLs.
When you're working in markdown it's not quite that easy though, because you don't have access to the asset
helper in markdown.
The solution is to write a Commonmark extension to do modify the URL on the fly, as the document is being parsed.
If you aren't familiar with Commonmark extensions, they are a convenient way for you to add functionality to the markdown parser. There are a number of first party ones, as well as many, many community authored ones.
We're going to write one here to look for image nodes and wrap the URL in the asset
helper.
Writing the Extension
The first step is to create the extension class. We'll call it VaporAssetWrapping
and implement the ExtensionInterface
.
VaporAssetWrapping.php
<?phpnamespace App; use League\CommonMark\ConfigurableEnvironmentInterface;use League\CommonMark\Extension\ExtensionInterface; class VaporAssetWrapping implements ExtensionInterface{ public function register(ConfigurableEnvironmentInterface $environment) { // @TODO }}
There are a number of things you can do with an extension, we are specifically going to listen for the DocumentParsedEvent
and look for image nodes.
The register
method is the entry point, where we're going to add our listener. We'll also set up an empty method that will contain all of our logic.
<?phpnamespace App; use League\CommonMark\ConfigurableEnvironmentInterface;use League\CommonMark\Event\DocumentParsedEvent;use League\CommonMark\Extension\ExtensionInterface; class VaporAssetWrapping implements ExtensionInterface{ public function register(ConfigurableEnvironmentInterface $environment) { $environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']); } public function onDocumentParsed(DocumentParsedEvent $event) { // @TODO } }
Once the document is parsed we're going to walk through all the nodes and look for images. We can do this by comparing the current node to the Image
class that Commonmark provides.
As we're walking the document we going to do a few things:
- See if the node is an image. If not, skip it.
- Only stop as we're entering the node.
- See if the image source is external by checking to see if it starts with
http
. Skip if so. - Wrap the relative url in the
asset
helper and update the node.
<?phpnamespace App; use Illuminate\Support\Str;use League\CommonMark\ConfigurableEnvironmentInterface;use League\CommonMark\Event\DocumentParsedEvent;use League\CommonMark\Extension\ExtensionInterface;use League\CommonMark\Inline\Element\Image; class VaporAssetWrapping implements ExtensionInterface{ public function register(ConfigurableEnvironmentInterface $environment) { $environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']); } public function onDocumentParsed(DocumentParsedEvent $event) { $walker = $event->getDocument()->walker(); while ($event = $walker->next()) { $node = $event->getNode(); // Only look for image nodes, and only process them upon entering. if (!$node instanceof Image || !$event->isEntering()) { continue; } // If it's an absolute URL, skip it. if (Str::startsWith($node->getUrl(), 'http')) { continue; } // Pass it through to the `asset` helper. $node->setUrl(asset($node->getUrl())); } } }
Registering the Extension
Now that you have your extension all written, you need to register it with the Commonmark parser.
If you are using Graham Campbell's markdown package then you just need to add it to your extensions
key in the markdown.php
file.
config/markdown.php
return [ 'extensions' => [ // The location of our static assets varies based on wherever // Vapor puts them. This will point them to the right spot. VaporAssetWrapping::class, ],];
If you aren't, then you'll need to manually register it:
$environment = Environment::createCommonMarkEnvironment();$environment->addExtension(new VaporAssetWrapping);
That's all you need to do! Let me know on twitter if it worked for you!