Skip to main content

Creating an OG image using React and Netlify Edge Functions

· 4 min read
Nick Taylor
AI Engineer

Open Graph (OG) images are a must if you're sharing content on the Internet. From sites like X/Twitter, to Slack or Discord, a great OG image makes your link share pop.

Examples

I recently built out a couple of OG images for Open Sauced for a couple of features we've rolled out over the past couple of months, Workspaces and Repository pages. They're great features that I encourage you to check out, and I encourage you to share them on socials so our beautiful OG images pop.

For example, here's an OG image for a workspace for jsr. JSR is the new JavaScript registry from the folks from Deno.

OG image for the OpenSauced workspace for jsr

And here's the OG image for a repository page for huggingface/transformers.

OG image for the huggingface/transformers repository

Looking at the image for the jsr workspace, there is a template for the image, but there are several dynamic portions to the image.

All the sections denoted by green outlined squares are dynamic.

OG image for the OpenSauced workspace for jsr with sections outline in green squares denoting dynamic portions of the image

This dynamic info gets pulled in for the most part from the OpenSauced API.

Other parts are pulled in from the URL, like 30 for the day range, and the description comes from the query string in the OG image URL.

browser dev tools view of the metadata section of the head with OG image URLs outlined by green squares

React to generate an image

So, how do we use React to generate an image?

We're using og_edge from my old co-worker Matt Kane (@ascorbic), but og_edge is a direct port of @vercel/og that works on Deno and Netlify Edge Functions which run on Deno.

https://github.com/ascorbic/og-edge

Under the hood, og_edge and @vercel/og use the Satori library.

Satori: Enlightened library to convert HTML and CSS to SVG.

https://github.com/vercel/satori

The API for the og_edge module is pretty straightforward. It exposes an ImageResponse constructor with the following options and that's it.

{% raw %}
new ImageResponse(
element: ReactElement,
options: {
width?: number = 1200
height?: number = 630
emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' | 'fluent' | 'fluentFlat' = 'twemoji',
fonts?: {
name: string,
data: ArrayBuffer,
weight: number,
style: 'normal' | 'italic'
}[]
debug?: boolean = false

// Options that will be passed to the HTTP response
status?: number = 200
statusText?: string
headers?: Record<string, string>
},
)
{% endraw %}

Code snippet above care of the official og_edge API reference.

To build out these OG images, we have a background image, some icons, like a star and fork icon, and we also pull in the repository organization or user's avatar. With a bit of vanilla CSS, we can position things just right. We also pull in the Inter font as that's what we use at OpenSauced.

As far as I know, og_edge does not support Tailwind like @vercel/og does. Not a dealbreaker at all, but just something to be mindful of.

One other thing we do is set cache headers as these are dynamic images where the data changes over time. Having said that, some social networks cache OG images very aggressively.

{% raw %}
headers: {
// cache for 2 hours
"cache-control": "public, s-maxage=7200",
"content-type": "image/png",
},
{% endraw %}

Show me the code

Here's the pull requests for the initial work on these two OG images.

https://github.com/open-sauced/app/pull/2939 https://github.com/open-sauced/app/pull/3117

Wrapping up

Beautiful and dynamic OG images are a must if you're looking to stand out when sharing links on socials, and og_edge and @vercel/og are great options if you also want to leverage your existing React skill set.

Now go out and build your own OG images! 🖼️

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Meet the New AI Engineers

· 5 min read
Bekah Hawrot Weigel
Developer Experience Lead

Chances are, you've heard of Devin, the AI software engineer that's "going to take your job." No one will argue that AI is changing the way things are happening in tech. But now it's a little different. Now, AI isn't just helping software engineers, it's becoming an engineer - doing things that only humans used to be able to do. So has it already grown beyond a tool? Is it now a partner in the engineering process? We're seeing examples of this with Devin, Devika, and OpenDevin – all AIs claiming to be really good at software engineering. Here's the big question: can AI actually write secure code all by itself? Is the future of coding in the hands of AI?

Form and Function: How I Lost My Submit Button & Got It Back

· 4 min read
Nick Taylor
AI Engineer

As web developers, we know that most of the create, read update, and delete (CRUD) actions we perform on the web are typically (hopefully?) done using an HTML form.

HTML Forms

HTML Forms are cool because they have plenty of built-in features.

For example, they have native form validation and access to all the inputs in a form, and at some point, because you need to submit the form, there is a mechanism to do that as well. You can use a button, <button>submit</button> or an input of type submit, <input type="submit" />, although the latter isn't used as much these days in new sites, from what I've seen.

Here is a simple form to exhibit this.

https://codepen.io/nickytonline/pen/JjVOarX

If you fill out the form and click submit, the form will submit and add a paragraph with dark green text client-side that says, "Form submitted".

Submitting the simple form

There are other things in the simple form, like form validation via the required attribute on inputs, but that's not what we're here to discuss.

What we want to touch on is that the form was able to handle the submit event because it had a submit button associated with it, which was defined in HTML within the form element.

Note: you can press enter in fields to submit a form, but again, not what we're here to discuss.

How I Broke My Form

This brings us to a new feature that I was working on for OpenSauced for a few months, workspaces. I encourage you to create your own, but for now, let's get back to the business of forms.

Here's our beautiful workspaces settings page that I implemented.

an OpenSauced workspace settings page

Recently, there were styling changes, which is what you see above.

https://github.com/open-sauced/app/pull/2982

Everything looked great, and I had tested it.

Narrator: he thought he had tested it, and we shipped things to production.

Once things went live, I decided to do some smoke tests, which I usually do. I went over to the beautiful workspace settings I had worked on, made some changes in the settings, and then clicked Update Workspace button. Hmm, no toast message saying the settings were updated. I checked the browser dev tools to see if there were any JavaScript errors. Nothing related to the updates. And then it dawned on me. The submit button was outside the form, and I just broke some key functionality in the app.

Michael Scott telling everybody not to panic.

Side note, but luckily thanks to Netlify's deployment rollback feature, I was able to revert to the previous production deployment within about a minute of the workspace settings page being broken 😅

How I Fixed My Form

So how did I fix it? We needed this new styling to fix several other issues related to z-indexes and layout.

For some context, the OpenSauced application is a Next.js site, so React, but I decided to put on my old school HTML hat and remembered that form elements can be associated to a form via a form attribute. What you need to do is give the form an id attribute, and the form element that you want to associate the form to needs to have a form attribute whose value is the value of the id attribute for the form.

Here's another simple form demonstrating a simplified version of my fix.

https://codepen.io/nickytonline/pen/XWQzPOX

I encourage you to remove the form attribute from the button in the above CodePen to see the issue I caused.

Here's the fix I rolled out to production.

https://github.com/open-sauced/app/pull/3003

Wrapping Up

Learning a framework is great, and I'm a big proponent of just building something, but as you continue on in your career, it's great to start getting some fundamentals into the mix.

Also, this is a perfect example of why using semantic HTML is important! It definitely helped me get out of jam! 😅

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

How to Build Your Open Source Dream Team

· 6 min read
Bekah Hawrot Weigel
Developer Experience Lead

We've talked a lot about the challenges of being a maintainer, especially a solo maintainer. But like I say in our recent newsletter, open source is a team sport. But finding the right teammates can be a tricky situation as well. You might not get it right all of the time. And that's ok. Taking steps to make sure you and your project stay healthy can help to create a smoother and more rewarding open source experience. In this blog post, I'll provide a checklist and examples to help you build and manage an effective team for your open source project.

Stuck in the Middle with You: An intro to Middleware

· 8 min read
Nick Taylor
AI Engineer

Middleware exists in several frameworks like Next.js, Express, Hono and Fresh, and not just in JavaScript land. You can find it in frameworks like ASP.NET core in the .NET ecosystem, or Laravel in PHP. Since I mainly work in JavaScript and TypeScript these days, examples will be from frameworks in those ecosystems.

Middleware is something that happens in the middle of a user or some service interacting with a site or API call and happens at the framework level.

It runs before a page loads or an API endpoint is called, or more generally a route. There are many reasons why you might want to do that:

  • gate certain content, e.g. a private route
  • set and read cookies
  • add headers to the response being sent out
  • URL redirect, e.g. redirecting to another page based on some criteria
  • URL rewrites

Let's dig in!

Gate Content

Authentication and authorization are two great candidates for guarding certain routes, although it’s still good to guard access to privied resources in API endpoints and pages. In this context, think of the middleware as the first line of defense.

Gandfalf saying, "You shall not pass!"

In the OpenSauced application, when a user logs in and the path is /workspaces we redirect them to their workspace.

{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);

return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}

Code on GitHub

Setting and Reading cookies

So what is a cookie?

A cookie is a way to set a piece of user-specific data. This could be a session ID for someone who is logged in to a site, or it could be some other user data. Note that the data in a cookie is typically not that large, but according to MDN, there is no size limit to the name or value of a cookie.

Cookie monster eating cookies

Cookies that are HTTP only can be accessed on the server-side, but for cookies that are not HTTP only, they can be accessed server-side and client-side. For example, you wouldn't want someone to tamper with your session ID on the client-side, so this type of cookie is set as HTTP only.

We recently shipped a new feature at OpenSauced, called Workspaces. You can read all about it in this great post from my co-worker Bekah (@BekahHW).

https://dev.to/opensauced/navigating-the-challenges-of-scaling-open-source-projects-11h2

TLDR; a Workspace acts like a workspace in other software you may have used, like Notion. One thing required for this feature is when a user navigates to the /workspaces URL, it has to load the last accessed workspace. If a user has never accessed a workspace before, it should default to their personal workspace. This is a perfect use case to leverage using a cookie.

When someone logs in, we check if they have a workspace ID cookie set. If they don’t, we grab their personal workspace ID, a type of workspace every user has.

The code for this was in the code snippet in the last section.

{% raw %}
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);
{% endraw %}

Let's take a peek into the getWorkspaceUrl function.

{% raw %}
export function getWorkspaceUrl(cookies: RequestCookies, baseUrl: string, personalWorkspaceId: string) {
if (!cookies.has(WORKSPACE_ID_COOKIE_NAME)) {
cookies.set(WORKSPACE_ID_COOKIE_NAME, personalWorkspaceId);
}

// @ts-expect-error the cookie value will be defined
const workspaceId = cookies.get(WORKSPACE_ID_COOKIE_NAME).value;

return new URL(`/workspaces/${workspaceId}`, baseUrl);
}
{% endraw %}

If there is no workspace cookie set, we create a cookie and set its value to the user's personal workspace ID.

After that, we read the cookie, we build a URL with it and the user is redirected to the workspace.

The other piece of this that doesn't occur in middleware is when a user visits a valid workspace page they have access to, we set the workspace ID cookie. Next time they go to the /workspaces link, the cookie will exist, and a URL using new URL() will be used to redirect them to the last accessed workspace homepage.

The page will call the OpenSauced app's setCookie function.

{% raw %}
export function setCookie({
response,
name,
value,
maxAge = 31536000,
sameSite = "Lax",
}: {
response: Response;
name: string;
value: string;
maxAge?: number;
sameSite?: SameSite;
}) {
response.setHeader(
"Set-Cookie",
`${name}=${value}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=${sameSite}; Secure`
);
}
{% endraw %}

Code on GitHub

Although cookies are their own thing, you have set them in a header.

Add Headers

As mentioned in the previous section, you set cookies via a header. So what is a header, more specifically, an HTTP header?

Headers are a set of key value pairs to let a browser know how to behave, for example, should a page be cached? It can also be custom key value pairs that your application or services might need. For example, when I worked at Netlify, for the CDN to work, there would be Netlify-specific headers that once inside the internal network would allow Netlify to do some magic.

If you go to my website, nickyt.co, and open the network panel in the dev tools of your browser of choice, you'll see some Netlify-specific headers.

Response headers from nickyt.co's homepage showing some custom Netlify headers being set

I recently gave a talk on Fresh, a full-stack framework for Deno at Node Summit '24. The recording isn't up yet, but here's the slide deck and code from the demo for anyone interested.

In Fresh middleware, this is how you could set a header.

{% raw %}
export async function handler(
request: Request,
ctx: FreshContext<State>
) {
const response = await ctx.next();
response.headers.set("x-fresh", "true");

if (request.url.includes("joke-of-the-day")) {
response.headers.set("x-joke-page", "true");
}

if (request.url.includes("movie/")) {
response.headers.set("x-movie-page", "true");
}

return response;
}
{% endraw %}

Code on GitHub

In the above code snippet, we're checking to see if a specific route contains a certain string and if it does, we set a custom header, e.g.

{% raw %}
response.headers.set("x-joke-page", "true");
{% endraw %}

URL Redirection

Page redirection allows you to have a URL go to another URL. You might do this for a couple of reasons. Maybe a bunch of links on your site changed, and you need to have them go to a new set of links, or you have a URL that needs to redirect to a user-specific page.

Kermit the frog looking at a map trying to figure out where to go

For non-trivial redirects like the workspaces redirect URL mentioned in one of the previous sections, middleware is a great place for handing redirects.

{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);

return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}

Code on GitHub

In this case, when someone in the OpenSauced application goes to /workspaces we redirect them to a user-specific URL.

{% raw %}
return NextResponse.redirect(`${workspaceUrl}`);
{% endraw %}

Not a hard and fast rule, but if you have trivial redirects like redirect /old-blog-path/* to /blog/*, consider using your hosting platform's redirects instead of middleware.

URL Rewriting

You can also do URL rewrites. It's like a redirect, but the URL never changes. Frameworks like Next.js provide this out of the box in their configuration file, but for more complex handling, you may want to do it in middleware. So what is a URL rewrite? A rewrite will preserve the existing URL but will render content from another URL.

Mr. Burns from the Simpsons saying, "Well, cover it with a rewrite"

Here's a slightly modified example straight out of the Next.js middleware documentation:

{% raw %}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
{% endraw %}

Code in Next.js documenation

In the above snippet, all users have a /dashboard page they go to, but every user's dashboard is different. In this case, the user will always see the page as /dashboard but it loads the specific user's dashboard.

Resources

Here's the documentation for middleware of the mentioned frameworks:

Wrapping Up

Middleware is a great tool and if your framework of choice supports middleware (most do), I encourage you to read up on how to leverage it in that framework.

What use cases have you used middleware for? Please let me know in the comments.

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Nurturing Open Source Collaboration and Growth

· 4 min read
Bekah Hawrot Weigel
Developer Experience Lead

Today is day 29 of my 29 Days of Open Source Alternatives series, where I explored open source alternatives to proprietary software in the categories of Game Development and Multimedia, Development Tools and Platforms, Productivity and Collaboration Tools, and more. If you'd like to see the full list of the open source alternatives I covered, head over to my 29 Days of Open Source Alts Page.

It's been almost a year since I started at OpenSauced. It's incredible how much we've grown and accomplished over that year. I say this very genuinely. Our team looks at problems and finds ways to solve them. We think about the users and how we can provide them value. We understand the importance of Open Source Insights, especially since GitHub deprecated their Activity Overview of Organization Insights on January 5. We recognize the importance of recognizing contributors and recognizing maintainers. This is a bit of a meta blog post. I want to take the time to recognize my team and the great contributors we have.