defmodule OGMate do
@moduledoc """
Builds PNG images at compile time from a user-provided key list and
content function. Always returns `{:ok, bytes}` from `image_for/1`.
Unknown keys resolve to the pre-built default image.
## Example
defmodule MyApp.OGImage do
use OGMate,
all_keys: ["home", "about"],
content_for: MyApp.OGContent,
theme: [
background: "#0a0a0a",
foreground: "#ffffff",
font: "Inter",
secondary: "#a3a3a3",
logo: "priv/static/images/logo.png",
site_name: "myapp.com"
],
default: {"MyApp", "A brief site description."}
end
{:ok, png} = MyApp.OGImage.image_for("home")
## Custom Renderer
Pass a `renderer:` module that exports `render(title, description)`:
defmodule MyApp.OGRenderer do
def render(title, description), do: {:ok, png}
end
defmodule MyApp.OGImage do
use OGMate,
all_keys: [...],
content_for: ...,
renderer: MyApp.OGRenderer,
default: {...}
end
`:theme` and `:renderer` are mutually exclusive.
See `OGMate.Options` for the full option contract, and the
[Getting started guide](getting_started.md) for end-to-end setup.
"""
# ── Using macro ──────────────────────────────────────────────────
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@before_compile OGMate
@og_opts opts
end
end
@doc false
defmacro __before_compile__(env) do
opts = Module.get_attribute(env.module, :og_opts)
options = OGMate.Options.validate!(opts)
{default_title, default_description} = options.default
default_bytes =
OGMate.__render__!(options.theme, options.renderer, default_title, default_description)
images =
unless options.dev_mode do
Map.new(options.all_keys, fn key ->
{title, desc} = options.content_for.content_for(key)
{key, OGMate.__render__!(options.theme, options.renderer, title, desc)}
end)
end
quote do
@og_default unquote(Macro.escape(default_bytes))
@og_images unquote(Macro.escape(images))
@og_content_for unquote(options.content_for)
@og_theme unquote(Macro.escape(options.theme))
@og_renderer unquote(Macro.escape(options.renderer))
@spec image_for(String.t()) :: {:ok, binary()}
def image_for(key) do
with :error <- lookup(key) do
{:ok, @og_default}
end
end
def default_image, do: @og_default
if @og_images do
defp lookup(key) do
Map.fetch(@og_images, key)
end
else
defp lookup(key) do
with {title, desc} <- @og_content_for.content_for(key) do
{:ok, OGMate.__render__!(@og_theme, @og_renderer, title, desc)}
end
end
end
end
end
@doc false
def __render__!(theme, renderer, title, desc) do
result =
case {theme, renderer} do
{%OGMate.Theme{} = t, nil} -> OGMate.Renderer.render(title, desc, t)
{nil, mod} when is_atom(mod) -> mod.render(title, desc)
end
case result do
{:ok, bytes} -> bytes
{:error, reason} -> raise "OGMate render failed: #{inspect(reason)}"
end
end
end