README.md

# OGI (Oh Gee)

Generates and serves OpenGraph Images using Typst.

Inspired by [OG-Image](https://github.com/svycal/og-image/tree/main) 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](https://peterullrich.com)

![](example.png)

## Installation

```elixir
def deps do
  [
    {:ogi, "~> 0.2.1"}
  ]
end
```

## Setup

You need these three things:

1. A Typst template.
2. A Phoenix Controller and Route.
3. An `og:image` metatag in your `<head>` tag.

### 1. The Typst Template

LLMs are pretty good at generating those and you can test them quickly on
[typst.app/play](https://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

### 2. The Phoenix Controller and Route

Below is an example controller for serving OG Images for a blog post.

```elixir
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)[By <%= author %>]
    """
  end
end
```

Then add this route to your router:

```elixir
scope "/", BlogWeb do
  get "/og-image/:id", ImageController, :show
end
```

### 3. The Metatag

For adding dynamic Metatags, I recommend the
[Metatags](https://github.com/johantell/metatags) library:

```elixir
# 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}
end
```

And 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.

## Configuration

Currently, OGI supports the following configurations:

```elixir
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"
```

## Caveats

### Adding Fonts and Images

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.

If you have the `typst-cli` installed on your system, you can run `typst fonts`
to list all available fonts.

For remote deployment, 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`.

## Examples

You can find Livebooks with examples of various use-cases in the `examples` folder.

- [Embedding Images](/examples/embedded-images.livemd)
- [Using Emojis](/examples/emojis.livemd)
- [Reusing Templates](/examples/templates.livemd)

## ToDo's

- [ ] Clean up Cache when a certain size is reached
- [ ] Allow async rendering. Useful for cache warmup.
- [x] Emoji Support
- [x] Support for templates
- [x] Allow per-request disabling cache operations
- [x] Add fallback OG Image option if render fails
- [x] Make cache dir path configurable.
- [x] Unit tests 😬