http_inertia

Hex Docs Hex.pm License

http_inertia is a server-agnostic Inertia protocol adapter for Gleam. It works with gleam/http request values and supplies the server-side pieces needed to build Inertia responses: page-object encoding, initial-page markup, request inspection, and prop-selection helpers.

It does not depend on a particular HTTP server or response type. Your web framework remains responsible for turning the generated page data into an HTTP response and serving the Inertia client assets.

Installation

gleam add http_inertia

Creating a page

Create a Page with a component name, JSON props, and an asset version. Encode it with page_component_json when returning an Inertia response.

import gleam/json
import http_inertia

let page =
  http_inertia.page(
    component: "users/index",
    props: [
      #("users", json.array(["Ada"], of: json.string)),
      #("errors", json.object([])),
    ],
    version: http_inertia.StringVersion("asset-version"),
  )

let page_json = http_inertia.page_component_json("/users", page)

For the initial non-Inertia visit, app_script produces the #app mount element and the JSON page-data script expected by the Inertia client:

let elements = http_inertia.app_script("/users", page)

Request helpers

Use is_inertia_request to distinguish an Inertia visit from an initial page load. request_url returns the path and query string for the current request.

For partial reloads, use is_partial_reload_for together with should_include_prop before doing work to load optional props:

let include_permissions =
  http_inertia.is_partial_reload_for(req, "users/index")
  && http_inertia.should_include_prop(req, "users/index", "permissions")

should_skip_once_prop applies the corresponding request rules for props registered with once_prop and with_once_props.

Protocol metadata

Start with page, then use the with_* functions to attach optional Inertia metadata:

See the http_inertia module documentation for each function’s API and the Mist example for a complete server integration.

Mist integration example

The following web module can be used for building a Mist integration with Vite frontend (also support dev mode). For a complete example of application, see the directory in the package repository.

import gleam/bytes_tree
import gleam/http.{type Method, Get, Head}
import gleam/http/request.{type Request as HttpRequest}
import gleam/http/response
import gleam/json
import gleam/list
import gleam/option.{None}
import http_inertia
import lustre/attribute
import lustre/element
import lustre/element/html.{html}
import mist

pub type Context {
  Context(static_directory: String, assets: Assets)
}

pub type Assets {
  ProductionAssets
  DevelopmentAssets(base_url: String)
}

pub type Request =
  HttpRequest(mist.Connection)

pub fn require_methods(
  req: Request,
  allowed allowed: List(Method),
  next handler: fn() -> Response,
) -> Response {
  case list.contains(allowed, req.method) {
    True -> handler()
    False ->
      text_response(405, "Method not allowed")
      |> response.set_header("allow", allowed_header(allowed))
  }
}

pub fn inertia_response(
  req: Request,
  ctx: Context,
  status: Int,
  title: String,
  page: http_inertia.Page,
) -> Response {
  let url = http_inertia.request_url(req)

  case http_inertia.is_inertia_request(req) {
    False -> {
      let body =
        html([], [
          html.head([], [html.title([], title)] |> list.append(asset_tags(ctx))),
          html.body([], http_inertia.app_script(url, page)),
        ])
        |> element.to_document_string
        |> bytes_tree.from_string

      response.Response(
        status: status,
        headers: [#("Content-Type", "text/html; charset=utf-8")],
        body: mist.Bytes(body),
      )
    }
    True -> {
      let body =
        http_inertia.page_component_json(url, page)
        |> json.to_string
        |> bytes_tree.from_string

      response.Response(
        status: status,
        headers: [
          #("X-Inertia", "true"),
          #("Content-Type", "application/json"),
        ],
        body: mist.Bytes(body),
      )
    }
  }
}

fn asset_tags(ctx: Context) -> List(element.Element(msg)) {
  let Context(assets: assets, ..) = ctx
  case assets {
    ProductionAssets -> [
      html.link([
        attribute.rel("stylesheet"),
        attribute.href("/static/assets/app.css"),
      ]),
      html.script(
        [
          attribute.type_("module"),
          attribute.src("/static/js/app.js"),
        ],
        "",
      ),
    ]

    DevelopmentAssets(vite_origin) -> [
      html.script(
        [
          attribute.type_("module"),
          attribute.src(vite_origin <> "/@vite/client"),
        ],
        "",
      ),
      html.script(
        [attribute.type_("module")],
        react_refresh_preamble(vite_origin),
      ),
      html.script(
        [
          attribute.type_("module"),
          attribute.src(vite_origin <> "/src-inertia/app.tsx"),
        ],
        "",
      ),
    ]
  }
}

fn react_refresh_preamble(vite_origin: String) -> String {
  "import RefreshRuntime from \""
  <> vite_origin
  <> "/@react-refresh\"\n"
  <> "RefreshRuntime.injectIntoGlobalHook(window)\n"
  <> "window.$RefreshReg$ = () => {}\n"
  <> "window.$RefreshSig$ = () => (type) => type\n"
  <> "window.__vite_plugin_react_preamble_installed__ = true\n"
}

Then you can define an application like:

import env
import gleam/erlang/application
import gleam/erlang/process
import mist
import web

pub fn main() {
  let ctx = web.Context(static_directory: static_directory(), assets: assets())
  let handler = make_handler(ctx)

  let assert Ok(_) =
    handler
    |> mist.new
    |> mist.bind("127.0.0.1")
    |> mist.port(8080)
    |> mist.start

  process.sleep_forever()
}

fn static_directory() -> String {
  // Change "demo_web" to the name of your application
  let assert Ok(static_directory) = application.priv_directory("demo_web")
  static_directory <> "/static"
}

fn assets() -> web.Assets {
  // This example uses an environment variable to run in development mode
  case env.get("DEMO_WEB_ENV") {
    Ok("development") ->
      web.DevelopmentAssets(base_url: vite_origin() <> "/static")

    _ -> web.ProductionAssets
  }
}

fn make_handler(ctx: web.Context) -> web.Handler {
  fn(req: web.Request) -> web.Response {
    use req <- web.middleware(ctx, req)

    case request.path_segments(req) {
      [] -> home(req, ctx)

      // This matches all other paths.
      _ -> web.not_found()
    }
  }
}

fn home(req: web.Request, ctx: web.Context) -> web.Response {
  use <- web.require_methods(req, [Get, Head])

  let page =
    http_inertia.page(
      component: "home",
      props: [
        #("errors", json.object([])),
      ],
      version: http_inertia.NullVersion,
    )

  web.inertia_response(req, ctx, 200, "Demo Home", page)
}

Further resources

Acknowledgements

wisp_inertia by Keuller Magalhaes inspired this package.

Search Document