I’m trying to implement a semi-interactive CMS (content management system) that supports directories for publishing simple documents (think blog posts or the like). I’m trying to implement a clean URL structure that confirms to good web practices, ie that is legible and where each hierarchy makes sense. This means i’m trying to avoid adding fake routes such as “/d/document-name” or the like. Documents start with a numerical ID, and are technically easy to distinguish from directories (that don’t have such an ID).
I want to support a URL structure such as this:
I’m using phoenix live views for this, but cannot figure out a good way to do this that doesn’t seem hacky. The root problem here is that the supported routes are somewhat inflexible, more specifically from what i can tell only “dynamic” paths supported are via a single glob pattern “*” in the router.ex that in turn is associated with a single liveview.
Here’s an example from my router.ex:
live "/*path", DocumentLive.Index, :index
This means that I seemingly have to use the same liveview for showing a directory (with previews of documents) vs showing an individual document. Not being able to distinguish at the router level is obviously bad.
To contrast this to django, you’d just use a regex like query to distinguish between a document and a directory, and then route it to different views.
Anyone have any suggestions as how to get around this without reverting to bad web URLs? I also really want to avoid creating one monolith liveview to do this. Plugs? Any suggestions appreciated!
To address this first, it’s not obviously bad. It has its benefits namely that routes are compiled and therefore can be validated at compile time. Of course it means in these more edge-case scenarios you need to get a little creative, but it’s not so bad!
I have done this kind of thing using a single LiveView with LiveComponents. You could do something roughly like this:
scope "/" do
live "*path", FileSystemLive
end
defmodule MyAppWeb.FileSystemLive do
use MyAppWeb, :live_view
@impl true
def mount(%{"path" => path}, _session, socket) do
case MyApp.Files.get_directory_or_document_from_path(path) do
{:ok, file} ->
{:ok, assign(socket, :file, file)}
{:error, changeset} ->
# handle error
{:ok, socket}
end
end
@impl true
def render(%{file: %MyApp.Files.Directory{}} = assigns) do
~H"""
<.live_component
id="directory"
module={MyAppWeb.DirectoryComponent}
directory={@file}
/>
"""
end
def render(%{file: %MyApp.Files.Document{}} = assigns) do
~H"""
<.live_component
id="document"
module={MyAppWeb.DocumentComponent}
document={@file}
/>
"""
end
end
Then you can separate out your logic and templates in the live components.
It’s not just that, but the Phoenix.Router (and Plug.Router) uses pattern matching to very efficiently select the matching route. That however means it’s bound by what can be expressed as a (binary) pattern to match on.
One could surely build alternative routers making different tradeoffs, but given how LVs and the phoenix router are coupled for features related to live navigation it’s certainly not as straight forward as things would be outside of LV.
I decided not to mention this since when I first ran into the same problem back before compiled routes my initial thought was “Meh, I only have a few routes and I’d rather have more flexibility than faster matching.” Though I pretty quickly grew out of that mindset, especially when realizing the work-arounds are not bad at all.
I guess a workaround could be having a plug before the router, which hoists the actual path into params or conn.assigns and makes the path some plain /directory or /file. Given these paths are fully dynamic things like verified routes are useless anyways.
Also, I’m not so sure these are definitely more web-friendly URLs… I’d at least need a citation For example, since the directories don’t have ids in their slug, you can’t rename them without invalidating the URL. Obviously there are ways around this, like keeping a history, but that’s more moving parts.
With non-router approaches, directory slugs could also have ids and then you can figure out in in the DB if something is a directory or document.
Supporting these kind of “dynamic” paths could certainly be done at compile time. It’s definitely a shortcoming that any other web framework i’ve worked with so far hasn’t had. It’s one of the most basic web hierarchies that doesn’t work out of the box for phoenix routers.
This is how google defines good URLs:
I like the following shorter and more explicit definition (even back from 1999), which notably includes “hackable” URLs, meaning you can remove the last part of the URL and it still shows relevant information.
Most importantly, URLs are part of the user interface and should be designed human-first, rather than tech-first. Unfortunately phoenix’s router pushes you more into a tech first, less user-friendly design.
I really dislike the idea of having liveview components living in the same “mega-liveview”. they should have totally different views.
Right now i’m probably going to route this by using plugs to determine the URL structure (ie :is_directory vs :is_document) and use an entirely separate scope for the documents vs the directories. This of course will slow down liveview, as not sharing scope means it’ll re-initiate the web socket data when navigating between views…
If I were you I will just bypass the router altogether; it is not useful to you, and your needs can be satisfied by fairly small amount of code. Remember everything is a plug, including router, controller, or liveview entrance point.
Not really an option as i’m using phoenix’s auth system and a bunch of other views.
Turns out, plugs don’t work to discriminate between directories and documents, as you’d have to explicitly set the redirect within the plug, which obviously doesn’t work given the router restrictions.
I’ve settled on just an entirely different flat hierarchy for the documents, ie a sub-optimal ‘/documents/12/document-name’. Looks dumb, isn’t hierarchical, and generally less user friendly as you can’t hack off the last bit of a url and get something reasonable.
The upside is that i can link to the same document from different directories, without it hurting the SEO/google ranking.
I might revisit this in the future, or if someone comes up with a cleaner idea than just using two separate live-components. Anyways, thanks for the input!
The auth system is also made of plugs. If you dig a little deeper you will see nothing is irreplaceable. Though I understand you may not want to look under the kimono until you get more comfortable in hacking or more annoyed by not getting what you want.