Generates and serves OpenGraph Images using Typst.
Inspired by OG-Image but uses Typst instead of Chrome+Puppeteer, so you can add it directly to your Phoenix app.
Generates (beautiful?) share images like this one for my blog peterullrich.com
def deps do
[
{:ogi, "~> 0.2.2"}
]
endYou need these three things:
- A Typst template.
- A Phoenix Controller and Route.
- An
og:imagemetatag in your<head>tag.
LLMs are pretty good at generating those and you can test them quickly on typst.app/play
Make sure that your markup follows the best-practices of OpenGraph Images which are:
- Dimensions of ideally
1200x630 - No more than
5MB - A bit of filled margin at the edges to prevent cropping of text
Below is an example controller for serving OG Images for a blog post.
defmodule BlogWeb.ImageController do
use BlogWeb, :controller
alias Blog.Posts
def show(conn, %{"id" => blog_id}) do
post = Posts.get_post_by_id!(blog_id)
assigns = [title: post.title]
opts = [typst_opts: [root_dir: typst_root(), extra_fonts: [fonts_dir()]]]
Ogi.render_image(conn, "#{blog_id}.png", typst_markup(), assigns, opts)
end
# these paths need to be called at runtime for releases
defp typst_root, do: Application.app_dir(:blog, "priv/typst")
defp fonts_dir, do: Path.join(typst_root(), "fonts")
defp typst_markup do
# Your Typst markup goes here.
#
# You can dynamically inline variables with:
# Blog Title: <%= title %>
#
# Note: There is *no* @ before the variable, other than in HEEx templates!
#
# Example:
"""
#set page(
width: 1200pt,
height: 630pt,
margin: 64pt,
fill: rgb("#0a1929")
)
#set text(size: 64pt, fill: white)
#place(center + horizon)[Hello World!]
#place(center + bottom)[Post: <%= title %>]
"""
end
endThen add this route to your router:
scope "/", BlogWeb do
get "/og-image/:id", ImageController, :show
endFor adding dynamic Metatags, I recommend the Metatags library:
# In your Controller or LiveView serving the blog post, add this:
def handle_params(%{"id" => post_id}, _url, socket) do
post = Posts.get_post_by_id!(post_id)
socket =
socket
|> Metatags.put("og:title", post.title)
|> Metatags.put("og:description", post.description)
|> Metatags.put("og:image", url(~p"/og-image/#{post_id}"))
{:ok, socket}
endAnd that's it! You can test this by navigating to the route manually or by using a browser extension that previews OpenGraph information for a website.
Typst has access to system fonts, as well as fonts in directories specified by the extra_fonts option. If a font is unavailable, Typst will fallback to a serif font, unless you set fallback: false on a #text. In this case Typst will simply not render the text at all.
It is recommended to bundle fonts with your application. The example above places fonts in the priv/typst/fonts directory, and images and other file resources in priv/typst.
When adding fonts, make sure to add non-variable fonts (e.g. FiraSans-Bold.tff and FiraSans-SemiBold.tff etc.) instead of variable fonts (e.g. FiraSans.tff) because Typst does not support variable fonts (yet)! If you add a variable font, Typst will always render the same font weight.
Currently, OGI supports the following configurations:
config :ogi,
# Whether to cache rendered images or not (default: true)
cache: true,
# Where to store the cache. Defaults to a temporary folder.
cache_dir: "./some/custom/dir",
# An optional fallback image which is returned if the rendering
# of the OG Image using Typst fails.
fallback_image_path: "./priv/static/some-image.png"You can find Livebooks with examples of various use-cases in the examples folder.
- Clean up Cache when a certain size is reached
- Allow async rendering. Useful for cache warmup.
- Emoji Support
- Support for templates
- Allow per-request disabling cache operations
- Add fallback OG Image option if render fails
- Make cache dir path configurable.
- Unit tests 😬
