# NimblePublisher (/integrations/nimble-publisher)

[Full example on GitHub](https://github.com/leandrocp/lumis/tree/main/packages/elixir/lumis/examples/nimble_publisher)

`NimblePublisher` ships with Makeup-based highlighting, but it also exposes a custom `html_converter` hook. That is the right integration point for Lumis.

Use `MDEx` as the markdown engine inside that converter. This is cleaner than post-processing HTML, and it keeps syntax highlighting inside the markdown renderer where it belongs.

## Keep `highlighters` empty

Do not configure NimblePublisher's built-in highlighters when Lumis owns code blocks.

```elixir
use NimblePublisher,
  build: Post,
  from: Application.app_dir(:my_app, "priv/posts/**/*.md"),
  as: :posts,
  html_converter: MyApp.Blog.Markdown,
  highlighters: []
```

## Add dependencies

```elixir
def deps do
  [
    {:nimble_publisher, "~> 1.0"},
    {:mdex, "~> 0.14"},
    {:lumis, "~> 0.10"},
    {:lumis_wasm_elixir, "~> 0.26.0"},
    {:lumis_wasm_bundle_web, "~> 0.1"}
  ]
end
```

MDEx takes Lumis as an optional dependency, so `:lumis` has to be declared — it
raises without it — and `:mdex_native` has to be told which highlighter to
download before it compiles:

```elixir
# config/config.exs
import Config

config :mdex_native, syntax_highlighter: :lumis
```

MDEx reads `lumis_wasm_*` parser packages. Add one per language your posts use,
or a bundle for a set of them. A fence whose language is not installed renders
as plain text. The [language catalog](/reference/languages) lists the Hex
package names.

For upgrades from MDEx releases with compiled-in grammars, see
[MDEx parser packages](/migrate/mdex).

## Custom converter

NimblePublisher converts posts while `MyApp.Blog` compiles, and `mix compile`
starts no applications. Starting `:mdex` brings up `:mdex_native`, whose
`start/2` is what hands its NIF the `lumis_wasm_*` directories to load parsers
from, so a converter has to do it. Skip it and every fence still renders through
the formatter — Lumis markup, theme background, one line span — around
plain, uncolored code, which reads as a theme problem rather than a missing
parser.

```elixir
defmodule MyApp.Blog.Markdown do
  def convert(_path, body, _attrs, _opts) do
    {:ok, _} = Application.ensure_all_started(:mdex)

    MDEx.to_html!(body,
      syntax_highlight: [
        engine: :lumis,
        opts: [formatter: {:html_inline, theme: "catppuccin_latte"}]
      ]
    )
  end
end
```

That also means there is nothing to warm at boot for these posts: they were
highlighted during `mix compile`, before `start/2` ever runs.
`Lumis.Languages.async_load/1` from your application's `start/2` is for Markdown
you render per request. Both sides share the cache of compiled parsers, so the
build leaves a language warm for the runtime when that cache persists between
them.

## Multi-theme version

```elixir
MDEx.to_html!(body,
  syntax_highlight: [
    engine: :lumis,
    opts: [
      formatter:
        {:html_multi_themes,
         themes: [
           light: "catppuccin_latte",
           dark: "catppuccin_frappe"
         ],
         default_theme: "light-dark()"}
    ]
  ]
)
```