inlay
Inlay is a library which renders embedded previews for social links (Mastodon, Pixelfed, Apple Music, Bluesky, Spotify, etc..) as part of (Blogatto) content or Lustre views.
Supported providers
Mastodon, Pixelfed, Apple Music, Bluesky, Spotify, Instagram, OpenStreetMap, SoundCloud, TED, TikTok, Twitch, Twitter/X, Vimeo, and YouTube
Installation
gleam add inlay
Configuration
inlay.new() has all providers disabled – you have to opt in to what you need (any other links will fall through and not have an embed). inlay.default_config() starts with some providers enabled – you can opt out of what you don’t want.
Opt-in with new()
This is the recommended approach to avoid unexpected embeddings with links on your website.
let config =
inlay.new()
|> inlay.mastodon(MastodonConfig(servers: ["mastodon.social"]))
case inlay.embed_with(url, config) {
Some(element) -> element
None -> html.text("Not embeddable")
}
Disabling providers
let config =
inlay.default_config()
|> inlay.no_twitter()
|> inlay.no_tiktok()
Provider-specific config
let config =
inlay.default_config()
|> inlay.youtube(YoutubeConfig(no_cookie: False))
|> inlay.twitch(TwitchConfig(parent: "mysite.com"))
|> inlay.mastodon(MastodonConfig(servers: ["mastodon.social", "fosstodon.org"]))
Bluesky
Bluesky embeds need an AT Protocol URI (at://did:plc:.../app.bsky.feed.post/...) to render the rich embed widget. When the post URL already contains a DID handle (e.g. did:plc:z72i7hdynmk6r22z27h6tvur), the embed works out of the box with the default config.
For human-readable handles (e.g. alice.bsky.social) or custom domains (e.g. flowvi.be), you need to provide a resolve_handle function that resolves the handle to a DID.
Here is an example (but your implementation may depend on whether you’re targetting javascript or erlang):
import gleam/dynamic/decode
import gleam/httpc
import gleam/http/request
import gleam/json
import gleam/option.{Some}
import gleam/result
import inlay.{BlueskyConfig}
let resolve = fn(handle) {
let url =
"https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle="
<> handle
use req <- result.try(request.to(url) |> result.replace_error(Nil))
use resp <- result.try(httpc.send(req) |> result.replace_error(Nil))
json.parse(resp.body, decode.at(["did"], decode.string))
|> result.replace_error(Nil)
}
let config =
inlay.default_config()
|> inlay.bluesky(BlueskyConfig(resolve_handle: Some(resolve)))
Lustre
Get an embedded view in a Lustre component:
import gleam/option.{None, Some}
import inlay
import lustre/element/html
pub fn view(url: String) {
case inlay.detect(url) {
Some(embed) -> inlay.render(embed)
None -> html.p([], [html.text(url)])
}
}
embed()
Render an embed for the provided url:
case inlay.embed("https://open.spotify.com/track/4PTG3Z6ehGkBFwjybzWkR8") {
Some(element) -> element
None -> html.text("Not embeddable")
}
detect() + render()
When you need access to the Embed value before rendering (e.g. for pattern matching):
case inlay.detect("https://youtu.be/dQw4w9WgXcQ") {
Some(embed) -> inlay.render(embed)
None -> html.text("Not embeddable")
}
Pattern matching on Embed
The Embed type is a public tagged union, so you can match on it for per-provider control:
case inlay.detect(url) {
Some(inlay.YoutubeVideo(id, ..)) -> custom_youtube_player(id)
Some(inlay.SpotifyMedia(..) as embed) -> html.div([class("spotify-wrapper")], [inlay.render(embed)])
Some(embed) -> inlay.render(embed)
None -> html.a([attribute.href(url)], [html.text(url)])
}
Server-side rendering
Lustre elements can be rendered to HTML strings:
case inlay.embed("https://vimeo.com/148751763") {
Some(el) -> element.to_string(el)
None -> "<p>Not embeddable</p>"
}
Blogatto
Blogatto’s markdown renderer lets you replace how specific HTML tags are produced. Inlay provides a custom <a> tag handler – when the href points to an embeddable URL, the link is replaced with an embedded preview. Non-embeddable links pass through to a fallback function. You can intercept and further customize this behavior if needed.
a_component_default()
Default handler with standard anchor fallback:
let md =
markdown.default()
|> markdown.markdown_path("./blog")
|> markdown.a(inlay.a_component_default())
a_component(fallback)
If Inlay doesn’t render an embed, you can control what happens to the link with a custom fallback.
For example, let’s make sure external links open in a new tab:
let my_a = fn(href, title, children) {
let attrs = case string.starts_with(href, "http") {
True -> [attribute.href(href), attribute.target("_blank"),
attribute.attribute("rel", "noopener noreferrer")]
False -> [attribute.href(href)]
}
let attrs = case title {
Some(t) -> [attribute.title(t), ..attrs]
None -> attrs
}
html.a(attrs, children)
}
let md =
markdown.default()
|> markdown.markdown_path("./blog")
|> markdown.a(inlay.a_component(my_a))
Development
gleam test # Run the tests
gleam build # Build for both Erlang and JavaScript targets
Further documentation can be found at https://hexdocs.pm/inlay.