How can I dynamically select what liveview to mount on a specific route

I have a liveview app that has some routes. Let’s say I have a route as an example:

scope "/", MyAppWeb do
    pipe_through [:browser]

    live_session :current_user,
      on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
      live "/:user_id", ProfileLive, :show
    end
  end

For SEO purpose I what to have shorter and seo friendly routes.
Instead of clasic paths like this:

/search/city/chicago
/profile/dentist-john-doe

I would like to have it like this:

/chicago
/dentist-john-doe

Where the first path should render a search result for all the specialists in Chicago city and the second route will render the profile of a specific specialist.
I would like to receive that parameter, do a query in DB first to see if I have a specialist with that id. Then take a decision: If I find the specialist render specialist page, if not I search if I have a city with that id and render the search result page for that city.
Is this even possible in liveview?

There may be a more elegant way to do this with plugs—I never actually figured out a way to mount different LiveViews at the same path at the router level (though I didn’t try very hard). One way I’ve done roughly what you’re describing in the past is to have one LiveView and use LiveComponents for the templates/callbacks. Something along the lines of this:

defmodule MyAppWeb.PageLive do
  @impl true
  def mount(%{"id" => id}, _session, socket) do
    record =
      with nil <- MyApp.Specialists.get_specialist(id) do
        MyApp.Cities.get_city(id)
      end

    {:ook, assign(socket, :record, record)}
  end

  @impl true
  def render(%{record: %MyApp.Specialists.Spcialist{}} = assigns) do
    ~H"""
    <.live_component id="specialist" module={MyAppWeb.SpecialistComponent} specialist={@record} />
    """
  end

  def render(%{record: %MyApp.Cities.City{}} = assigns) do
    ~H"""
    <.live_component id="city" module={MyAppWeb.CityComponent} city={@record} />
    """
  end

  def render(%{record: nil} = assigns) do
    ~H"""
    <div>Not found</div>
    """
  end
end

In your router you need a catch-all route after all other routes:

live "/:id", PageLive, :index

I suppose you could also do this using a classic controller which dispatches to different LiveViews using live_render in a similar way as above, then you don’t have to use LiveComponents.

It’s probably also better to move the with logic into your domain layer and having something like record = MyApp.Search.find_specialist_or_city, but not a big deal either way depending on how complex your app is.

1 Like

Afaik the idea that URL length does matter for SEO is a big myth. Especially with people interacting less and less with URLs to begin with I probably wouldn’t care.

The issue with (router mounted) LVs specifically is that on establishing the websocket connection is needs to be able to find the live … route using Phoenix.Router.route_info, which doesn’t work well with your idea of having the route be dynamically determined. You could generate the routes at compile time to work around that, but it doesn’t sound like you know the short urls at compile time. Knowing them only at runtime you’d end up with what @sodapopcan described, which works, but is not an architecture that’s meant to be used.

A simpler way to accomplish both should be sticking to the existing live routes and have a controller, which redirects the shorts urls to their long ones.

1 Like

Google cares much more about the semantics, including in urls these days. As long as you dont duplicate (keep urls canonical) content, the first set of urls you showed are much better.

1 Like

Thanks guys for feedback.
I decided to stick with liveview routes even if the path has 2-3 levels instead of trying to make it one level and doing a lot of magic. I’ll keep my fingers crossed when I’ll release it live.
I will add also the canonical url to point to the same page to not have duplications in case of extra parameters.

1 Like

How would you deal with a situation like github usernames where the username is the root path segment?

Otherwise I’ve only used my solution for an artsy site where I literally only had two routes: / and /:slug. I should have caveated that, though I’m still curious about your thoughts on the username thing. My solution would, of course, only be looking for :user_id at the bottom of the router, not returning a variable resource.

Dynamic segments which result in the same “kind of page” are fine. The thing I meant to say being discouraged is having a single LV manage multiple “kinds of pages”. And the edge between those certainly are not clear cut. Even usage of live action I’ve heard being discouraged by chris mccord, unless there’s a reason for the pages to be related, like the modal case, where the modal section still renders the underlying page as well.

2 Likes

Awesome, thanks for clarifying!

I actually didn’t know that about @live_action though that is the only case I use it for: routable edit modals.

Source? :slight_smile:

Somewhere deep in the depth of inaccessible slack conversations.

Could you put a toy example for this ?

Thank you in advance.