Lynx: Dynamic Linking
Automatically embed links in content uploaded by your users. Learn how to generate link previews for articles and other rich media.
Installation
Add Lynx to your dependencies in mix.exs
:
defp deps do
[
# Other deps
{:lynx, "~> 0.1", organization: "equip"}
]
end
Note: In fresh Phoenix projects, :floki
is added as a :test
dependency. To
install Lynx, you may have to change {:floki, ">= 0.0.0", only: :test}
to
{:floki, ">= 0.0.0"}
.
Basic usage
At it’s simplest, Lynx is used to, well, render links. We can change URLs in a
body of text to links using Lynx.Text
:
iex(1)> Lynx.HTML.linkify_text("Take me to example.com!")
"Take me to <a href=\"http://example.com\">example.com</a>!"
The linkify_text/2
function also accepts arguments that give the user more
control over how links are rendered, for example:
iex(1)> Lynx.HTML.linkify_text "Take me to example.com!",
...(1)> process_href: &Routes.exit_path(@conn, :exit, path: &1)
"Take me to <a href=\"http://myapp.com/exit?path=example.com\">example.com</a>!"
(The above examples will actually render iodata
, not binaries. Binaries are shown for readability.)
For all options, see the docs.
Configuration
What if we want to add other, special kinds of links to our app? With Lynx, you can customize what is parsed and how values are formatted.
config :lynx,
parser: MyApp.Lynx.CustomParser,
formatter: MyApp.Lynx.CustomFormatter
Example parsing module:
defmodule MyApp.Lynx.CustomParser do
use Lynx.Parser,
strategies: [
Lynx.Parser.strategy(:link),
Lynx.Parser.strategy(:mention),
cashtag: ~r{\$\w+}u
]
end
Parsing strategies are simply a key paired with a RegExp for isolating values. Strategies take preference in the order they’re defined.
defmodule MyApp.Lynx.CustomParser do
use Lynx.Parser,
strategies: [
Lynx.Parser.strategy(:link),
Lynx.Parser.strategy(:mention),
cashtag: ~r{\$\w+}u
]
end
A custom formatter must implement functions to handle each strategy defined in the parser.
defmodule MyApp.Lynx.CustomFormatter do
def format({:cashtag, value}), do: String.upcase(value)
end
Link Previews
Lynx can help to generate “link previews” for any links that are submitted to your application. To get started, run the generator:
mix lynx.create MyContext
This will produce the following results:
* creating lib/my_app/my_context/link_preview.ex
* creating priv/repo/migrations/20210115215417_create_link_previews.exs
* creating lib/my_app/my_context/link_preview_pub_sub.ex
* creating lib/my_app/my_context.ex
* injecting lib/my_app/my_context.ex
Take a look at link_preview.ex
, this is the schema we will be using to persist
new link previews to the database.
In my_context.ex
(or, however you named your context), you can see that helper
functions have been created for managing link previews. These functions will be
used by Lynx.
The link_preview_pub_sub.ex
file contains a module for assisting with how your
app is notified about inserted and updated link previews.
Next, let’s add the LinkPreview.Server
to our supervision tree:
children = [
# Other children
{Lynx.LinkPreview.Server, context_module: MyApp.MyContext}
]
The context module must be configured either here or in the :lynx
config.
Now we can submit text to the server. It will parse out any links and send them
to the default LinkPreview.Client
. Here, we’ll submit a Post
to the link
preview server. By default, submit_resource/1
looks for a :text
field.
def create_post(user = %User{}, attrs) do
%Post{}
|> Post.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert()
|> Lynx.LinkPreview.Server.submit_resource()
end
Our context module functions are already set up to broadcast updates when link previews are inserted or updated, so to add real-time link updating, we have to listen to these updates in our LiveView.
defmodule MyAppWeb.PostLive do
use MyAppWeb, :live_view
alias MyApp.MyContext
alias MyApp.MyContext.LinkPreview
@impl true
def mount(%{"id" => post_id}, _session, socket) do
post = MyContext.get_post!(post_id)
# If connected to the live view, subscribe to link previews for this post
if socket.connected? do
LinkPreview.PubSub.subscribe_link_preview(post)
end
{:ok, assign(socket, :post, post)}
end
@impl true
def handle_info({:link_preview, _message}, socket) do
# Reload the post (with link previews)
post = MyContext.get_post!(socket.assigns.post.id)
{:noreply, assign(socket, :post, post)}
end
end
It’s possible to configure the client that handles fetching data for links. By default, pages are fetched and then parsed following the Open Graph protocol.
config :lynx,
client: [
strategy: Lynx.LinkPreview.OpenGraphClient
]