MishkaCmsCore.ExtensionRouter behaviour (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Behaviour and macro for extensions to define their routes.

Extensions implement this behaviour to register their admin, public, and API routes. The main CMS router uses extension_routes/1 macro to load all extension routes.

Usage

In your extension:

defmodule MishkaBlogWeb.Routes do
  @behaviour MishkaCmsCore.ExtensionRouter

  @impl true
  def routes(:admin) do
    [
      {:live, "/blog/posts", MishkaBlogWeb.Live.Admin.PostLive, :index}
    ]
  end

  def routes(_), do: []
end

In your main router:

import MishkaCmsCore.ExtensionRouter

scope "/admin/dashboard" do
  pipe_through [:browser, :require_authenticated_user]

  live_session :admin do
    extension_routes(:admin)
  end

  live_session :admin_standalone do
    extension_routes(:admin_standalone)
  end
end

In config.exs:

config :mishka_cms, :extensions, [
  MishkaBlogWeb,
  MishkaDocumentWeb
]

Summary

Callbacks

Returns the routes for the given type.

Functions

Macro to inject all extension routes into your router.

Phoenix.Controller.accepts/2 for a pipeline that pages and files share: the formats the matched route names under its :accepts private, or formats for a route that names none.

Types

route()

@type route() ::
  {:live, String.t(), module(), atom()}
  | {:live, String.t(), module(), atom(), keyword()}
  | {:get | :post | :put | :patch | :delete, String.t(), module(), atom()}
  | {:get | :post | :put | :patch | :delete, String.t(), module(), atom(),
     keyword()}

route_type()

@type route_type() :: :admin | :admin_standalone | :public | :api

Callbacks

routes(route_type)

@callback routes(route_type()) :: [route()]

Returns the routes for the given type.

Types:

  • :admin - Admin dashboard routes
  • :admin_standalone - Admin routes that need a document of their own (see below)
  • :public - Public-facing routes
  • :api - API routes

When a route belongs in :admin_standalone

Only when the page needs something in <head> that the rest of the admin does not.

The root layout decides <head> from the LiveView's assigns, and it renders once per HTTP response. A live navigation does not produce one: the destination LiveView runs inside the document the SOURCE page built, with the source page's head. A page whose stylesheets, scripts or meta tags differ from its neighbours' therefore arrives half-dressed, with no error anywhere — the page builder reached this way lost its Tailwind JIT and the shim that nonces what it creates, and its code editor opened unstyled.

:admin_standalone routes are emitted into a live_session of their own, and LiveView will not live-navigate across a live_session boundary: arriving is a full page load, and so is leaving, which also sheds whatever the page installed. Navigation BETWEEN two routes in this list stays live, which is what makes it the right home for a group of pages that share a head.

Everything else belongs in :admin. The boundary costs a page load in each direction, and a page that is dressed like the rest of the admin has nothing to buy with it.

Functions

extension_routes(type)

(macro)

Macro to inject all extension routes into your router.

Extensions are configured in :mishka_cms, :extensions. This macro is evaluated at compile time - no runtime overhead.

route_accepts(conn, formats)

@spec route_accepts(Plug.Conn.t(), [String.t()]) :: Plug.Conn.t()

Phoenix.Controller.accepts/2 for a pipeline that pages and files share: the formats the matched route names under its :accepts private, or formats for a route that names none.

Plug it in place of plug :accepts, formats, imported from this module:

import MishkaCmsCore.ExtensionRouter

pipeline :browser do
  plug :route_accepts, ["html"]
  # ...
end

A route answering a file of another kind names what it answers, and every other route keeps formats — so /robots.txt answers Accept: text/plain while a page asked the same is still refused with a 406:

{:get, "/robots.txt", MyExtWeb.RobotsController, :show,
 private: %{accepts: ["html", "txt", "xml"]}}

A route serving a stored file as it is names :any, and answers whatever Accept it is sent, as Plug.Static does — a crawler asks for a gzipped sitemap as application/x-gzip, which no format names:

{:get, "/sitemap-:file", MyExtWeb.SitemapController, :file, private: %{accepts: :any}}

A route's private is on the conn before its pipeline runs, so this reads the route being served.