hmans.co https://hmans.co Fri, 28 Apr 2023 19:35:22 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://hmans.co/wp-content/uploads/2023/04/cropped-favicon-32x32.jpg hmans.co https://hmans.co 32 32 Automating OpenGraph Image Generation with Lume is surprisingly straight forward. https://hmans.co/automating-opengraph-image-generation-with-lume-is-surprisingly-straight-forward/ Sun, 13 Nov 2022 19:34:49 +0000 https://hmans.co/?p=34 Automating OpenGraph Image Generation with Lume is surprisingly straight forward. Read More »

]]>
Science has shown that people are a million times more likely to click on a link if it has an image. Maybe a bazillion times. You’ve probably reached this article because you saw a tweet linking to it, and you thought to yourself: holy cow, that’s an incredible banner image! I must click it!

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:

  • Finding out how to create a PNG file for every blog post on the site
  • Rendering the HTML and CSS to SVG via Satori
  • Converting that SVG to an actual PNG

Let’s go! 🚀

Creating a PNG for every blog post

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.

Rendering the HTML and CSS to SVG via Satori

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",
    },
  ],
};

Converting the SVG to a PNG

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!

Putting it all together

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:

  • Caching! At the moment this will regenerate all images for all posts every time the site is built. At the moment, with only a handful of posts, this is fine, but I expect it will get slow and annoying as the site grows.
  • Randomization! I could generate a random seed from the blog post title an use that to maybe randomize the background gradients a little, or introduce some other graphical elements. This could be fun!
  • Extract into a plugin! Yeah, this is a very obvious candidate for a Lume plugin, but I want to get to know the framework a little better first.
]]>
Lume is Great and I’m finally at peace. https://hmans.co/lume-is-great-and-im-finally-at-peace/ Thu, 10 Nov 2022 19:32:53 +0000 https://hmans.co/?p=32 Lume is Great and I’m finally at peace. Read More »

]]>
It was time to relaunch my little blog. It had been time to do this for literally months. This week, I finally set down and got started. Lots of cool new frameworks to try! I was looking for something that satisfied the following requirements:

  • Can render the site out to 100% fully static HTML, with no server or cloud functions required. This one is important to me because I need to know that I can throw my blog on any host that can serve static files. Some static sites I deployed almost a decade ago are still online today, for free, with no work required on my side, and I think that’s awesome, and find it very relaxing. I wouldn’t feel this way if the site required to live on, say, Vercel (and I’m saying this as someone who makes heavy use of that platform.)
  • Can render a static RSS and/or ATOM feed with the full HTML bodies of my posts. I was surprised how often this was simply not possible to do with the new generation of website frameworks. I was even more surprised how often people eventually just asked me who, in 2022, would even care about RSS feeds. You may not care about RSS feeds, but I do — and in my opinion a website framework that simply can’t render out non-HTML content is, at best, a toy, but certainly not an adequate tool for building a website. (Okay, I’m getting into ranting mode again. Stay tuned for angry future posts. :b)
  • Support for nested layouts/data. The web, just like a file system, is a tree of objects/documents. Let me make use of this when building my website, please. I want all posts to live in a posts folder. I want all of them to automatically inherit a post-specific layout. I was surprised to find how few frameworks embrace this.
  • Lets me author posts in Markdown, ideally MDX. This is pretty much a given in 2022, literally every framework supports this (and that’s awesome.)

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.

Batteries Included

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.

Hooray Nested Data!

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 %}

ESBuild for the bigger client-side stuff

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.

Customization

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.

Conclusion

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.

]]>
Miniplex 2.0 Beta 1 has been released, and it’s a big one! https://hmans.co/miniplex-2-0-beta-1-has-been-released-and-its-a-big-one/ Wed, 09 Nov 2022 19:29:23 +0000 https://hmans.co/?p=30 Miniplex 2.0 Beta 1 has been released, and it’s a big one! Read More »

]]>
Miniplex 2.0 Beta 1 is out, details here!

Okay, look, I messed up!

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.

Miniplex 2.0

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!)

Buckets, Buckets, Buckets!

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.

Next Steps

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.

]]>