I'm building out a Shedquarters in my backyard! Check out the ever-evolving post + pictures here →

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

1<?php
2namespace App;
3 
4use League\CommonMark\ConfigurableEnvironmentInterface;
5use League\CommonMark\Extension\ExtensionInterface;
6 
7class VaporAssetWrapping implements ExtensionInterface
8{
9 public function register(ConfigurableEnvironmentInterface $environment)
10 {
11 // @TODO
12 }
13}
Code highlighting powered by torchlight.dev.

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.

1<?php
2namespace App;
3 
4use League\CommonMark\ConfigurableEnvironmentInterface;
5use League\CommonMark\Event\DocumentParsedEvent;
6use League\CommonMark\Extension\ExtensionInterface;
7 
8class VaporAssetWrapping implements ExtensionInterface
9{
10 public function register(ConfigurableEnvironmentInterface $environment)
11 {
12 $environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']);
13 }
14 
15 public function onDocumentParsed(DocumentParsedEvent $event)
16 {
17 // @TODO
18 }
19 
20}

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.
1<?php
2namespace App;
3 
4use Illuminate\Support\Str;
5use League\CommonMark\ConfigurableEnvironmentInterface;
6use League\CommonMark\Event\DocumentParsedEvent;
7use League\CommonMark\Extension\ExtensionInterface;
8use League\CommonMark\Inline\Element\Image;
9 
10class VaporAssetWrapping implements ExtensionInterface
11{
12 public function register(ConfigurableEnvironmentInterface $environment)
13 {
14 $environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']);
15 }
16 
17 public function onDocumentParsed(DocumentParsedEvent $event)
18 {
19 $walker = $event->getDocument()->walker();
20 
21 while ($event = $walker->next()) {
22 $node = $event->getNode();
23 
24 // Only look for image nodes, and only process them upon entering.
25 if (!$node instanceof Image || !$event->isEntering()) {
26 continue;
27 }
28 
29 // If it's an absolute URL, skip it.
30 if (Str::startsWith($node->getUrl(), 'http')) {
31 continue;
32 }
33 
34 // Pass it through to the `asset` helper.
35 $node->setUrl(asset($node->getUrl()));
36 }
37 }
38}

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

1return [
2 'extensions' => [
3 // The location of our static assets varies based on wherever
4 // Vapor puts them. This will point them to the right spot.
5 VaporAssetWrapping::class,
6 ],
7];

If you aren't, then you'll need to manually register it:

1$environment = Environment::createCommonMarkEnvironment();
2$environment->addExtension(new VaporAssetWrapping);

That's all you need to do! Let me know on twitter if it worked for you!

Thanks for reading! My name is Aaron and I'm currently working at small property tax firm in Texas called Resolute Property Tax Solutions, where I serve in dual roles as COO & CTO.

I work on a lot of projects. I'm building a shedquarters. I currently do a podcast, and I used to do a different podcast.

If you ever have any questions or want to chat, I'm always on Twitter
Copyright 2013 - 2021, Aaron Francis.