Here’s how I added them to mine: I hired a guy, and he made them for me!
Then we had a huge falling out (he didn’t like my favorite HTML color, hotpink) and I ended up having to do them myself. Damn!
Obviously, I didn’t want to make them manually, so it was time to figure out how to automate their creation.
Just a few weeks ago, Vercel announced Vercel OG Image Generation, a feature of their platform that allows you to generate these images on the fly.
But now I faced a new problem: all their cool new bits were designed for use within Next.js, their React-based web application framework, while this new blog is just a statically generated website built with Lume (which I was gushing over in an earlier post.)
At the heart of their offering is Satori, a new library that can render HTML and (a subset of) CSS to SVG. It was time to find out how I can use it from within Lume. This encompassed three things:
Let’s go! 
Lume made this surprisingly easy. It has a concept of templates, special documents that can create not one, but multiple pages in the final build of the site. They are required to export a default generator function that yields a list of pages to be created.
For this site, this template started out simple, like this:
export default async function* ({ search }) {
/* Look through all pages whose `type` attribute equals `post` */
for (const page of search.pages("type=post")) {
yield {
url: `${page.dest.path}.png`;
content: "insert actual PNG bits here",
};
}
}
Just this little snippet made sure that for a blog at /posts/hello-world/, there would also be a /posts/hello-world/index.png.
I also added the Metas plugin to the site and configured it to default to use a relative-linked image.png like this:
metas:
image: ./index.png
# ...
This basically makes it so every page will default to a local index.png file for its OpenGraph image, while still allowing me to override it where I want to.
Now I needed to create some SVG for each post. Satori allows me to use JSX for this, which is a nice touch. It supports a subset of HTML and CSS, making heavy use of Flexbox for layout (as far as I understand, it uses Facebook’s Yoga layout engine.)
For this site, this ended up looking like this:
const svg = await satori(
<div
style={{
display: "flex",
height: "100%",
width: "100%",
padding: 60,
flexDirection: "column",
backgroundImage: "linear-gradient(to bottom, #222, #333)",
color: "rgba(255, 255, 255, 0.8)",
textShadow: "5px 5px 5px rgba(0, 0, 0, 0.5)",
}}
>
<div style={{ fontSize: 60, fontWeight: 700 }}>hmans.co</div>
<div
style={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
justifyContent: "flex-end",
}}
>
<div style={{ color: "hotpink", fontSize: 90, fontWeight: 700 }}>
{page.data.title}
</div>
<div style={{ fontSize: 60, fontWeight: 700 }}>{page.data.subtitle}</div>
</div>
</div>,
options
);
As you can see, it’s just JSX-ed HTML with inline styles.
The options object you can see at the end of that snippet configures output dimensions and available fonts. The latter were a little bit tricky because in order for Satori to even be able to render any text, you need to load the font files into memory and pass them to Satori.
I ended up downloading the Inter font from Google Fonts and loading them into memory like this:
const inter = await Deno.readFile("./src/fonts/Inter-Regular.ttf");
const interBold = await Deno.readFile("./src/fonts/Inter-Bold.ttf");
Now my Satori options looked like this:
const options: SatoriOptions = {
width: 1200,
height: 627,
fonts: [
{
name: "Inter",
data: inter,
weight: 400,
style: "normal",
},
{
name: "Inter",
data: interBold,
weight: 700,
style: "normal",
},
],
};
The last step was to convert the SVG to a PNG. For this, I used the Deno resvg-wasm package, a wrapper around the resvg SVG rendering library. This essentially boiled down to:
import { render } from "https://deno.land/x/[email protected]/mod.ts";
/* later */
await render(svgForPost(post));
Easy!
That’s it, that’s the whole thing! I’ve put the full template source up on GitHub if you want to take a look and/or steal it. Some potential improvements for the future:
posts folder. I want all of them to automatically inherit a post-specific layout. I was surprised to find how few frameworks embrace this.After the couple of disappointments that I hinted at above, I ended up with Lume, a static site generator build for Deno.
And it’s perfect. It literally checks every single one of the boxes listed above.
Lume is very similar to Eleventy in nature (and, in fact, inspired by it), so it’s more focused on emitting website documents than being an “application framework”. Unlike Eleventy, Lume made me productive much faster. In Eleventy, I always felt like adding website-typical tooling like Sass was weirdly complicated (but I admit it’s been 1.5 years since I last tried it, and it might have evolved since then.)
In Lume, many things are just a small plugin away, many of which are shipped with Lume itself. Adding Sass support was a matter of doing the following in my configuration file:
import sass from "lume/plugins/sass.ts";
site.use(sass());
Lume ships with a PageFind plugin that is just as easy to set up and “just works”:
import pagefind from "lume/plugins/pagefind.ts";
site.use(pagefind());
Now just add a <div; id="search"></div> to your site’s HTML, and you have a fully working full-text search for your site. Amazing.
Lume offers a simple, opinionated abstraction of your site: every document you create is a page, and every page has data. Pages can have content, but they can also be just data.
They can be written with Markdown (where their data is expressed as typical Markdown frontmatter):
---
title: Hello World
author: hmans
---
# Hello World!
But you can also write pages as JSX modules, where the default function renders the page body, and you use named exports for data:
export const title = "Hello World";
export const author = "hmans";
export default () => <h1>Hello World!</h1>;
Many other template languages are supported besides these two.
Default data for all documents in the same directory and its child directories can be set via a _data file, which can be authored in Yaml, JSON, or even be made dynamic with JavaScript and TypeScript. I’m using this in my blog to set a default layout and page type for all pages within the posts directory:
layout: post.njk
type: post
When it’s time to render a list of all blog posts, I can use Lume’s search helper to get a subset of all pages in the site:
{% for post in search.pages("type=post", "date=desc") %}
<p>
<strong>
<a href="proxy.php?url={{ post.data.url }}">{{ post.data.title }}</a>
</strong>
{{ post.data.subtitle }}
</p>
{% endfor %}
If you eventually do need to bundle some JavaScript that you need to run in the client, you can add the esbuild plugin, and it will automatically create a bundle from any .js(x) or .ts(x) file in your site. Adding something like Sandpack was extremely straight forward with this.
Here’s an example app where React is rendered server-side, but then hydrated in the client. It’s cool that this is possible!
I’m also enjoying that in Deno, you can just write your import statement, and the next time you start the app, it will fetch the specified dependency for you (from NPM, too!) and cache it locally. No need to run npm install or yarn install anymore.
Lume allows you to use the DOM API to process files before they get written to disk:
site.process([".html"], (page) => {
page.document?.querySelectorAll("img").forEach((img) => {
if (!img.hasAttribute("alt")) {
img.setAttribute("alt", "This is a random alt");
}
});
});
Creating your own plugins is exceptionally easy, as they mostly just encapsulate things you would otherwise be doing inside your site’s configuration file, like the snippet above. It’s simple and powerful, and I’m enjoying it a lot.
Lume is making me extremely happy. On Twitter, I called it “a cornucopia of sane design decisions”. It’s the first time in a long time that I feel like I can build a website without having to fight the tool I’m using to do it. Building this blog with it was huge amounts of fun, and I’m looking forward to using it in more web projects in the future.
In the beginning of 2022, I sat down and wrote my own little ECS implementation for JavaScript and TypeScript. It was designed to be friendly, easy to use, and generally prefer developer experience and familiarity over performance. It started life as hmecs, but I eventually renamed it to miniplex. And it was good! Very good!
Three quarters of a year later, it was time to finally release version 1.0 of the thing. I had been using it in many projects and libraries, and it was feeling extremely stable, so one day I decided to just go for it, and release the first stable major version. I did just that, posted an announcement on Twitter, had some people try it, and…
…it was a disaster!
Someone immediately identified a bug that prevented Miniplex from working correctly in React’s <StrictMode;> (this is how I found out that React-Three-Fiber doesn’t automatically inherit the outer <StrictMode;>, a bug that will luckily be fixed in that library’s next major version.)
But to make things worse, I failed at fixing this issue because I had made some critical mistakes in the library’s design.
So I sat down, just days after the 1.0 release, and… rewrote the entire thing.
The result of this rewrite is Miniplex 2.0, a much lighter and more relaxed, but at the same time more flexible and powerful take on the ECS pattern (and yes, it works in React’s <StrictMode;> now), and it’s now available as a Beta.
If you already have a project that uses Miniplex 1.0, the upgrade should be relatively painless. There are breaking changes in the API, but they’re almost exclusively surface-level API changes (renames, etc.). For example, world.createEntity is now just world.add, and world.destroyEntity is now world.remove. (There’s even a world.update now, and I will write more about it soon!)
The underlying architecture is all new, and allows for some interesting setups that may play an increasibly larger role in the future; for example, you can now derive an archetype from another archetype, building an entire tree of separate buckets of entities. Archetypes can now use predicate functions to filter entities, too! This is going to give you a lot of control in more complex setups.
For games, I’m working on instrumentation tooling that visualizes what is going on in your ECS layer, helping you to identify bottlenecks and other performance issues. Stay tuned for more on that later!
But if your needs are cozy and simple, rest assured that using Miniplex remains just as straight forward to use as you know it from version 1.0.
For a more complete list of changes, please see the Miniplex 2.0 Beta 1 Release Announcement.
I intend to leave Miniplex in Beta for at least another couple of weeks, to give people time to try it out and report any issues they may find. This also gives me some time to finalize the all-new documentation website.
I expect that there will be another two or three Beta releases, with a final 2.0 release happening around the beginning of 2023.
]]>