00:02
we'll see an image pop up as a preview of the link that we're sharing. That looks something like this here for the Atacast repository. I'm just on opengraph.xyz pointed to the Atacast
00:11
GitHub repository URL. And these are called open graph or OG images. And these are defined via meta tags. So for example, for the ones that we're seeing previewed over on the right-hand side,
00:20
the meta tags would look something like this, one of which is this meta property. And then the other one would look like this. And then the other one would look like this.
00:28
One of which is this meta property with OG image and the content pointing to the images URL. There's another one specific to Twitter, and there's several different purposes
00:37
why you might use an OG image. One of which is to just share a preview of the link that you're sharing itself. Another one is to help drive conversion to the link as well. In GitHub's case,
00:47
they're dynamically generating these OG images so that they can add additional information to them, like the number of contributors, issues, stars, forks, et cetera. But GitHub also just has a ton of repositories
00:57
that they're dealing with. So dynamically generating them is just a nice consistent way to show a preview for each and every repository that they host. What we're gonna do here today
01:05
is go over how we can generate dynamic OG images of our own, utilizing AdonisJS, EdgeJS to render them out, and then Puppeteer to actually print it out into an image.
01:15
So if I jump into my terminal, let's go ahead and just get a brand new project set up so that we're all starting from the same basis. So we'll do npm init AdonisJS at latest.
01:24
And I'm gonna call this dynamic OG images for the project name and enter to create that. And I'm gonna select the web starter kit so that we have EdgeJS set up and ready to go. I'm gonna skip authentication. We won't need it.
01:34
For the database driver, I am gonna select SQLite. We're gonna use rate limiting to set a limit on the number of OG images that can be generated at any one point in time
01:42
to limit the stressors against our server should this make its way out to production. So we'll select SQLite there or whatever database driver it is that you would like to use. And then that's gonna go ahead
01:52
and get things set up for us. Perfect, let's go ahead and CD into that folder and I'm gonna clear our terminal out. While we're in here, let's go ahead and install Puppeteer as we're gonna need that.
02:00
So we'll do npm i puppeteer and hit enter to install that. Great, let's go ahead and add in the rate limiter. So node ace add at AdonisJS limiter
02:10
and we'll hit true to go ahead and install that. The node ace add command will install as well as run the configuration step for the package itself, which we're seeing right here
02:20
which is asking which storage layer we'd like to use. I'm gonna go ahead and use database which is the SQLite that we set up there in the previous step. Perfect, no authentication or anything else that we need to change.
02:29
So I'm just gonna go ahead and node ace migration run to get the migration for the rate limits table created within the SQLite database.
02:38
And I'm gonna clear that out one more time and let's do node ace make controller now. And we'll call this our OG images controller
02:45
with an index and a render method inside of it. Perfect, you can go ahead and run npm run dev now to boot our server up and let's switch this out.
02:55
So let's go to localhost 3333, hit enter just to enter in the splash screen for the AdonisJS application and make sure it's all up and working.
03:04
Let's get this set up inside of our text editor next. So let's open it up, code series, scenarios, dynamic images, open. And let's start by just jumping into our routes
03:13
and getting those set up. So router.get slash OG hyphen images. And this one will go to our OG images controller
03:22
index method as OG images index. And we'll do another router.get slash OG images slash render
03:31
that points to our OG images controller render method as OG images render, perfect. We can then go ahead and jump into our resources,
03:40
views, pages, and let's just stub out a new page called OG underscore image dot edge. This is the page that we're gonna utilize puppeteer to actually take a screenshot of
03:50
to serve as our OG image. So I'm just gonna get a quick HTML5 documents set up here. And for simplicity sake,
03:57
let's go ahead and just use the TailwindCSS CDN. You can also load in your actual applications assets should you have any within here so that you keep the same colors,
04:07
configuration fonts, et cetera, that you're using inside of your actual application so that there's some consistency there. To give us a little bit of a way to tell what the actual boundaries of our images are,
04:17
I'm gonna go ahead and set a background color on the actual HTML document of just EBEBEB, which is a light gray. On the body itself, let's add in a class.
04:27
Let's set the width to 1200 pixels and the height to 630. That will match the OG image aspect ratio
04:36
that social platforms are expecting. And then I'm gonna pad this with a 24 there, set a border bottom of 12 pixels,
04:44
and we'll make that border a blue 600. And then we give ourselves a nice little background gradient to the bottom from a white via a blue 100
04:54
to a blue 300. All right, this is gonna serve as our actual image itself. So we'll just set that directly on the body. Inside of the body, we can do a div class flex justify
05:04
between and a gap of 12 with a div class flex one. And then we'll do an H1 with a class of text 7XL,
05:13
font bold, and then we'll render out some title text here. And then we'll do a paragraph with a class text 4XL,
05:20
text slate 700, and a margin top of six. And for the paragraph here, I'm just gonna paste in some lorem ipsum text. We could do the exact same thing here
05:30
that we're doing for the title as well for a description, but just for brevity's sake, we'll put some lorem there. And then let's also do an image.
05:37
So do a div class width 48 with an image source, and we'll need to grab an image for that with a class width full.
05:46
And I'm gonna put rounded 2XL on that. Okay, so I have an image copied here to serve in its place. I'll have a link out to this image from this repository.
05:55
What we wanna do is just right-click new folder within our project, create a public folder if you don't already have one. Perfect, and let's dive into that.
06:04
And I'm gonna paste the image just directly in there. This is the image itself right there with a file name of Atticast underscore icon. So for our image, we just need to point to that.
06:13
So Atticast underscore icon.png, anything inside of that public folder will be directly served from our running server. So we just do slash and then that file name.
06:23
That is it for our HTML markup here for our OG image. Next, we can go ahead and dig into our app controllers, OG images controller within our render method.
06:31
What we wanna do is take in the request and the view to render out that page that we just created. So we do const title equals, and then we can just grab this off of the query string
06:41
via request input title, and then we'll do some default text here if it's not provided. So something like hello from OG image,
06:49
and then we'll return view, render pages, OG image, and pass that title in. Again, if you wanna do a subtext, you could do the exact same thing here that we're doing for the title
06:59
where we have that lipsome text. All right, let's give that a save. We have our server running, so let's go and jump back into our browser and let's head on over to slash OG hyphen images
07:09
slash render, and we should see something like this. So we just get back our base text of hello from OG image. That serves as its placeholder for our title, and then our lipsome text with that little icon
07:19
over here to the right. If we were to provide in a title query string, so something like title, something here, hit enter, that title gets swapped out with our placeholder text
07:29
right here where we have that title being dynamically rendered out. Alternatively to query strings, you could also specify an entity's ID to fetch that from the database
07:38
to render out the actual database-driven data directly here where we have this text. Right now though, this is still just an HTML document. What we need to do is turn this into an actual image
07:48
so that we can use it as our OG image on social platforms. So back into our text editor, that's where our index method comes into play. So for this, we're gonna want our request
07:57
and our response from our HTTP context. We're gonna want a browser variable to hold an instance of the Puppeteer browser, and this will be a type browser from Puppeteer,
08:07
and it doesn't look like it's picking that up, so I'm gonna just import browser from Puppeteer, just like so, but we're gonna start it out as null. So we'll do or null equals null.
08:17
Then we want to do a try with a catch. Inside of this catch, we'll rethrow the error because what we really wanna do is, in the instance that we catch an error,
08:27
we want to shut down the Puppeteer browser that we have running. So we'll do browser, and remember it may or may not be null, so we'll use question mark dot
08:35
to call its close method if it's populated. This will ensure that we don't accidentally leave any Puppeteer browser instances running in the background should we run into an exception
08:45
at any point here within our try. Without this catch, you should kind of imagine that you left a Chrome tab open every single time that you visited a page.
08:53
That would kind of be akin to what you would be doing here without that catch should you run into an exception. So definitely want that there. Then we'll do a const URL equals new,
09:03
and we'll use the URL constructor here to build out the actual URL the page will be visiting. The page we'll be visiting is our ogImageRenderPage. So we'll use the router to make a URL
09:13
pointing to the og.images.renderRoute that we have set up within our route definitions. This accepts no query params, so we'll do an empty object there,
09:22
but we do wanna pass along the query strings since that's how we're passing along that dynamic data in this example. So we'll just use the request.queryString to pass that on along.
09:32
In addition to that, we want this to be an absolute URL. Right now this is gonna make it relative, so we want to provide in a base domain,
09:40
which will be HTTP localhost 3333. We could also easily set this up to a dynamic environment variable as well, since this would definitely differ
09:49
not only based on the port that our application's running on but also between our local and production environments. We'll keep it like that for right now though. From there, we want to set up our browser instance using Puppeteer.
09:59
So we'll await importPuppeteer. and launch a browser instance. Within here, we do want to provide in at least one argument. So we'll do args and we'll do hyphen hyphen,
10:09
disable dev shm usage. This will inform Puppeteer to launch the Chrome instance using a temporary directory
10:18
instead of the slash dev slash shm directory for memory management. And that may or may not help you out in production with garbage collection, cleaning up the instances of our Puppeteer browser here.
10:28
Once we have our browser launched, we want to get a new page set up. So we'll do page await browser.newPage, and we want to set the viewport for this page.
10:37
So we'll do page.setViewport. And we want to set this to the same width and height that we have defined on our body inside of the page itself,
10:44
which is 1200 for the width and 630 for the height. This will ensure that the screenshot that we end up taking
10:51
will only consist of the actual portion of the document that we want it to take a screenshot of. Once we have all that set,
10:58
we can await page goTo and point this to our URL. And we want to point it to as a string. So we'll do URL.toString there. But in addition to that,
11:07
we also want the page to wait for all of our assets and resources to load. We want to wait for the image to load. We want to wait for the Tailwind CDN to load.
11:15
So we want to inform it to wait until the network load is idle and we'll use the zero flag there. Once we have our page all set up and it's pointing to our URL,
11:24
that is when we can go ahead and take our screenshot. So we do const screenshotBuffer equals await page. And there is a screenshot method
11:34
that we can call directly with Puppeteer's page. And this accepts an argument set that we can define the image's type width.
11:42
So you can set that as JPEG, PNG, WebP. I'm going to go ahead and use JPEG here. And that is the crescendo of our method. Once we have our screenshot buffer, we can go ahead and start to clean up.
11:52
So we'll do await page close to close that page out. And then we'll await browser close to close the browser.
12:00
Then we need to send the response on out. So do response. And we'll add in a header to set the content type
12:08
of the response to image slash JPEG. If you selected WebP or PNG, you want to match that accordingly with the content type here. And then we'll use response
12:18
to just send our screenshot buffer on away. Give that a save. It'll do a little bit of formatting and whoops, doozle. These two arguments right here, the empty object in our query string,
12:28
I meant to have inside of our make URL. So let's plop those into there and give that another save. That looks more correct. Okay.
12:37
So we have our make URL pointing to our OG images render with an empty object and then passing back through our query string. So that query string gets passed through to our render method. Perfect.
12:47
So with all of that set, let's go ahead and jump back into our browser here. And we just want to get rid of the render. So the slash render, let's just get rid of that. And let's just go to our OG image index method.
12:57
Now, if we hit enter on that, it's going to take a second. You may get these notifications here. That's fine. That's just Chrome getting set up inside of a sandbox. I would go ahead and hit allow on those,
13:07
but we end up seeing our image get printed out here and we can tell it's an image because it looks different. It's being placed inside of our browser's image preview window. It still looks the exact same,
13:17
but it's now being sent through as an actual image. We can right click, inspect, take a look at our network, go ahead and reload. Again, it might take a second here. That's because it's rendering out,
13:26
taking a screenshot and all of that, launching the browser instance. And whoop, I just have this taking a look at XHR. We want to take a look at the entire picture here. So we have our OG image requests going out there.
13:36
The type is JPEG and it's being sent back as an actual image. If we take a look at the response, you could see right there, we have the MIME type of the image and the dimensions and all that fun stuff. Perfect.
13:46
Next up. So within our .env here, I'm just going to go ahead and copy what we have there. Jump into our .env and I'm going to add an environment variable just called app URL.
13:55
I have this equal to http//localhost colon. And then we can do relatively similar to string interpolation in JavaScript's backticks
14:04
to insert the port dynamically into this app URL. Jump into our starts, env.ts, and let's add this in as well.
14:12
So app URL, env, schema. We'll just set that up as a string since we have that port being dynamically added in. Give that a save.
14:19
And now we can go back into our OG images index method, replace this string here with an env, import that from our start env.get,
14:28
and reach through to our app URL. Give that a save. Jump back into our browser really quick. Give that a refresh. Just make sure that it's working okay. Perfect. Get back the exact same thing.
14:38
If we were to get rid of our title designation, that should go back to our default. Perfect. Okay. Now what we want to do is limit the number of puppeteer instances that we can have going
14:47
at any one point in time using rate limiting. Because I'm sure we would all much rather have the OG image generation fail out due to a rate limit rather than our server crashing
14:57
due to it being overloaded. So within our start directory, we should have this limiter set up. So let's jump into that file and we can export const
15:05
and create a throttle OG images limiter. So limiter.define. And I'm gonna call this OG images.
15:12
Pass in a callback that returns our limiter. And we'll allow requests. And this is gonna vary depending on the size of your box that your server's running on,
15:22
but just for easy testability, I'm just gonna go ahead and set this to three. So we'll allow three OG images to be created every, and let's say maybe 30 seconds. And we want to make sure that this is global.
15:32
So we'll set a specific key for all of these. So regardless of the OG image being created, we'll just use a global OG images rate limit key.
15:41
So that even if we're creating an OG image for three different resources, it'll count against the same rate limit to prevent that fourth from happening. Then that would just use a baseline exception message.
15:51
So we can go ahead and provide our own VD limit exceeded error callback here, just saying something like error.setMessage.
15:59
Too many OG image requests are being made at one time. Please wait before requesting another or whatever you'd like there.
16:09
Okay, give that a save, because now we need to go jump into our routes to add this, not necessarily into our render route. This is just going to render out an EdgeJS page
16:17
and plop it directly back as HTML. But our OG image index method where the actual Puppeteer instance is launched and the screenshot is taken.
16:27
So we'll use the use method here to apply our throttle OG image rate limitation. Give that a save. And now we should be able to jump back into here. Let's give it a refresh.
16:36
One, two, three, and now on the fourth, we should get rate limited. Too many OG images being requested at one time. Okay, and you can kind of mess around
16:45
with your rate limit there to get that going at a rate that is applicable for your application. That could be five seconds or whatever you see fit there. Perfect, so from here,
16:54
you have a couple of different options to improve this even further. You can use caching to directly cache the response of the image itself using something like Cloudflare. That's what I do with AttaCasts.
17:03
So that if Cloudflare has that image cached already, it won't even bother to hit your server to create the OG image using Puppeteer. We'll just directly serve it from Cloudflare
17:12
without even bothering to ping your server. Additionally, you can also store the image directly inside of somewhere like S3 for cloud storage, and then check to see if you already have
17:21
the OG image stored that's trying to be requested. If you do, serve it from your storage. If you don't, generate out the OG image using Puppeteer. So those are a couple of next steps that you can take.
17:30
Serving from the cache is going to obviously be the easiest, but there you have it.