Skip to content

Commit 566e746

Browse files
authored
Implements full interface and documentation (#1)
This consists of the full basic version of OGMate, with documentation and tests. There's lots that can be added in the way of theming, options, backgrounds, etc, but this is enough to get us started and the library provides an escape hatch where the user provides their own renderer function.
1 parent d91a1df commit 566e746

16 files changed

Lines changed: 929 additions & 33 deletions

File tree

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: "mix"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
cooldown:
9+
default-days: 7
10+
11+
- package-ecosystem: "github-actions"
12+
directory: "/"
13+
schedule:
14+
interval: "weekly"
15+
cooldown:
16+
default-days: 7

.github/workflows/ci.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Test (Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }}${{ matrix.unlock && ' / unlock' || '' }})
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
include:
18+
- elixir: "1.17.0"
19+
otp: "25.3"
20+
unlock: false
21+
- elixir: "1.19.5"
22+
otp: "28.5"
23+
unlock: false
24+
- elixir: "1.19.5"
25+
otp: "28.5"
26+
unlock: true
27+
28+
steps:
29+
- uses: actions/checkout@v6
30+
31+
- uses: erlef/setup-beam@v1
32+
with:
33+
elixir-version: ${{ matrix.elixir }}
34+
otp-version: ${{ matrix.otp }}
35+
36+
- name: Cache deps and build
37+
uses: actions/cache@v5
38+
with:
39+
path: |
40+
deps
41+
_build
42+
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
43+
restore-keys: |
44+
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
45+
46+
- name: Check formatting
47+
run: mix format --check-formatted
48+
49+
- name: Unlock deps
50+
if: matrix.unlock
51+
run: mix deps.unlock --all
52+
53+
- name: Install dependencies
54+
run: mix deps.get
55+
56+
- name: Compile with warnings as errors
57+
run: mix compile --warnings-as-errors
58+
59+
- name: Run Credo
60+
run: mix credo --strict
61+
62+
- name: Run tests
63+
run: mix test

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Johanna Larsson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
# OgMate
1+
# OGMate
22

3-
**TODO: Add description**
3+
An opinionated library for generating OG images for static and dynamic routes, mildly inspired by NimblePublisher.
44

5-
## Installation
5+
OG, or Open Graph, images are shown by social media and chat apps as a preview image when sharing links, and although they don't do anything for SEO, they can make your links look more professional (or fun).
6+
7+
OGMate is designed specifically to reduce the lift to get OG images going for your Elixir blog or site, with an opinionated basic template. The library also provides escape hatches for setting up your own image generation and just using the boilerplate to organize things. It's agnostic to your actual routes and metadata, you provide the implementation for fetching image keys, titles, and descriptions.
68

7-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `og_mate` to your list of dependencies in `mix.exs`:
9+
For an end-to-end setup including a Plug, layout integration, and optional dev mode config, see the [Getting started guide](https://hexdocs.pm/og_mate/getting_started.html). Basic examples available below.
10+
11+
## Installation
912

1013
```elixir
1114
def deps do
@@ -15,7 +18,80 @@ def deps do
1518
end
1619
```
1720

18-
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19-
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20-
be found at <https://hexdocs.pm/og_mate>.
21+
## Examples
22+
23+
Happy path example.
24+
25+
```elixir
26+
defmodule MyApp.OGContent do
27+
def content_for("home"), do: {"MyApp", "Welcome."}
28+
def content_for("about"), do: {"About", "My cool site"}
29+
def content_for(_), do: :error
30+
end
31+
32+
defmodule MyApp.OGImage do
33+
use OGMate,
34+
all_keys: ["home", "about"],
35+
content_for: MyApp.OGContent,
36+
theme: [
37+
background: "#0a0a0a",
38+
foreground: "#ffffff",
39+
font: "Inter",
40+
secondary: "#a3a3a3",
41+
logo: "priv/static/images/logo.png",
42+
site_name: "myapp.com"
43+
],
44+
default: {"MyApp", "A brief site description."}
45+
end
46+
47+
{:ok, png} = MyApp.OGImage.image_for("home")
48+
```
49+
50+
`MyApp.OGContent` exports `content_for/1` returning `{title, description}` for known keys or `:error` for unknown ones. Unknown keys resolve to the configured default image.
51+
52+
Or, replacing the template with your own renderer:
53+
54+
```elixir
55+
defmodule MyApp.OGRenderer do
56+
def render(title, description) do
57+
# Build your own image, return {:ok, png_binary} or {:error, reason}
58+
# Use the code from OGMate.Renderer as inspiration!
59+
end
60+
end
61+
62+
defmodule MyApp.OGImage do
63+
use OGMate,
64+
all_keys: ["home", "about"],
65+
content_for: MyApp.OGContent,
66+
renderer: MyApp.OGRenderer,
67+
default: {"MyApp", "A brief site description."}
68+
end
69+
```
70+
71+
You can either set `renderer` or `theme`, you can't set both.
72+
73+
## Theme
74+
75+
Required: `background`, `foreground`, `font`, `secondary`.
76+
Optional: `logo` (path to image file), `site_name` (text shown next to logo).
77+
78+
See `OGMate.Theme` for the full spec.
79+
80+
## Dev mode
81+
82+
By default, all images are built at compile time. For local dev, set `dev_mode: true` to render on demand instead of building everything up front:
83+
84+
```elixir
85+
use OGMate,
86+
all_keys: [...],
87+
content_for: MyApp.OGContent,
88+
theme: [...],
89+
default: {...},
90+
dev_mode: Application.compile_env(:my_app, :og_image_dev_mode, false)
91+
```
92+
93+
In dev_mode, each `image_for/1` call invokes `content_for/1` and the renderer at runtime. New content in your `content_for` module is visible without expensive recompilations.
94+
95+
## Dependencies
2196

97+
The default renderer uses [`:image`](https://hex.pm/packages/image), which bundles precompiled libvips binaries, no additional install needed. However, any fonts you want to use have to exist on the machine that builds the images.

guides/getting_started.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Getting started
2+
3+
TL;DR: define a content module mapping keys to titles and descriptions, point OGMate at it, mount a plug to serve `/images/og/<key>.png`, drop a meta tag in your layout. Images are built at compile time and served straight from memory.
4+
5+
OG images are those preview cards you see when sharing links on social media. They don't help SEO but they make your links look intentional. OGMate handles the build pipeline, you provide the content.
6+
7+
This walks through the full setup, framework-agnostic. Works inside a Phoenix endpoint, but Phoenix isn't required.
8+
9+
## 1. Add the dependency
10+
11+
```elixir
12+
def deps do
13+
[
14+
{:og_mate, "~> 0.1.0"}
15+
]
16+
end
17+
```
18+
19+
## 2. Content module
20+
21+
Map URL slugs to `{title, description}`. Return `:error` for anything unknown, those fall through to the default image at runtime.
22+
23+
```elixir
24+
defmodule MyApp.OGContent do
25+
@static_content %{
26+
"home" => {"MyApp", "Welcome to MyApp."},
27+
"about" => {"About", "What MyApp does."}
28+
}
29+
30+
def content_for(key) when is_map_key(@static_content, key),
31+
do: Map.fetch!(@static_content, key)
32+
33+
def content_for("posts/" <> id) do
34+
case MyApp.Blog.find_by_id(id) do
35+
nil -> :error
36+
post -> {post.title, post.description}
37+
end
38+
end
39+
40+
def content_for(_), do: :error
41+
end
42+
```
43+
44+
Static map for fixed pages, pattern-matched clauses for dynamic content. The catch-all `:error` clause at the bottom handles anything the rest didn't.
45+
46+
## 3. The OGMate module
47+
48+
```elixir
49+
defmodule MyApp.OGImage do
50+
use OGMate,
51+
all_keys:
52+
["home", "about"] ++ Enum.map(MyApp.Blog.all_posts(), &"posts/#{&1.id}"),
53+
content_for: MyApp.OGContent,
54+
theme: [
55+
background: "#0a0a0a",
56+
foreground: "#ffffff",
57+
font: "Inter",
58+
secondary: "#a3a3a3",
59+
logo: "priv/static/images/logo.png",
60+
site_name: "myapp.com"
61+
],
62+
default: {"MyApp", "Welcome to MyApp."},
63+
dev_mode: Application.compile_env(:my_app, :og_image_dev_mode, false)
64+
65+
def path_for(key) when is_binary(key), do: "/images/og/#{key}.png"
66+
end
67+
```
68+
69+
`all_keys` is computed at compile time. `MyApp.Blog.all_posts()` is a normal call to an already-compiled module, the result gets inlined as a literal list. New posts means a recompile, but Phoenix code reloading handles that for you in dev.
70+
71+
`path_for/1` keeps URL building in one place so the plug and your templates stay in sync.
72+
73+
## 4. The plug
74+
75+
A small plug intercepts `/images/og/<key>.png` and serves the bytes.
76+
77+
```elixir
78+
defmodule MyApp.Plugs.OGImage do
79+
@behaviour Plug
80+
import Plug.Conn
81+
82+
@impl Plug
83+
def init(opts), do: opts
84+
85+
@impl Plug
86+
def call(%Plug.Conn{request_path: "/images/og/" <> rest} = conn, _) do
87+
key = String.replace_suffix(rest, ".png", "")
88+
{:ok, bytes} = MyApp.OGImage.image_for(key)
89+
90+
conn
91+
|> put_resp_content_type("image/png")
92+
|> put_resp_header("cache-control", "public, max-age=31536000")
93+
|> send_resp(200, bytes)
94+
|> halt()
95+
end
96+
97+
def call(conn, _), do: conn
98+
end
99+
```
100+
101+
That `cache-control` is a year, which is fine because the URLs are content-keyed. If the content changes, the key changes.
102+
103+
## 5. Mount the plug
104+
105+
In Phoenix, drop it into the endpoint:
106+
107+
```elixir
108+
# lib/my_app_web/endpoint.ex
109+
plug MyApp.Plugs.OGImage
110+
plug Plug.Static, ...
111+
```
112+
113+
Before `Plug.Static` so OG requests are caught first. Outside Phoenix, add it to your `Plug.Router`.
114+
115+
## 6. Reference in your HTML
116+
117+
```html
118+
<meta property="og:image" content={MyApp.OGImage.path_for(@og_key)} />
119+
```
120+
121+
Set `@og_key` per route in your controller. `"home"` for the homepage, `"posts/#{post.id}"` for a post page.
122+
123+
## 7. Dev mode
124+
125+
Building everything at compile time is great for production. But rendering all your OG images on every save while you're iterating gets old fast. Set `dev_mode: true` and OGMate skips that at compile time, rendering lazily on each `image_for/1` call instead. New content shows up immediately, no recompile.
126+
127+
```elixir
128+
# config/dev.exs
129+
config :my_app, og_image_dev_mode: true
130+
```
131+
132+
```elixir
133+
# config/prod.exs
134+
config :my_app, og_image_dev_mode: false
135+
```
136+
137+
The `Application.compile_env(:my_app, :og_image_dev_mode, false)` call in step 3 reads this at compile time.
138+
139+
## See also
140+
141+
- `OGMate`: main module reference
142+
- `OGMate.Theme`: theme field reference
143+
- `OGMate.Renderer`: default renderer (1200×630 PNG layout)

0 commit comments

Comments
 (0)