README.md

<a href="https://github.com/jsonmaur/phoenix-pages/actions/workflows/test.yml"><img alt="Test Status" src="https://img.shields.io/github/actions/workflow/status/jsonmaur/phoenix-pages/test.yml?label=&style=for-the-badge&logo=github"></a> <a href="https://hexdocs.pm/phoenix_pages/"><img alt="Hex Version" src="https://img.shields.io/hexpm/v/phoenix_pages?style=for-the-badge&label=&logo=elixir" /></a>

Create blogs, documentation sites, and other static pages in Phoenix. This library integrates seamlessly into your router and comes with built-in support for rendering markdown with frontmatter, syntax highlighting, compile-time caching, and more.

- [Getting Started](#getting-started)
- [Formatting](#formatting)
- [Frontmatter](#frontmatter)
- [Syntax Highlighting](#syntax-highlighting)
- [Index Pages](#index-pages)
- [Defining Paths](#defining-paths)
- [Extended Markdown](#extended-markdown)
- [Local Development](#local-development)

## Getting Started

```elixir
def deps do
  [
    {:phoenix_pages, "~> 0.1"}
  ]
end
```

The recommended way to install into your Phoenix application is to add this to your `router` function in `lib/myapp_web.ex`, replacing `myapp` with the name of your application:

```elixir
def router do
  quote do
    use Phoenix.Router, helpers: false
    use PhoenixPages, otp_app: :myapp

    # ...
  end
end
```

Now you can add a new route using the [`pages/4`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#pages/4) macro:

```elixir
scope "/", MyAppWeb do
  pipe_through :browser

  get "/", PageController, :home
  pages "/:page", PageController, :show, from: "priv/pages/**/*.md"
end
```

This will read all the markdown files from `priv/pages` and create a new GET route for each one. The `:page` segment will be replaced with the path and filename (without the extension) relative to the base directory (see [Defining Paths](#defining-paths)).

You'll also need to add the `:show` handler to `lib/myapp_web/controllers/page_controller.ex`:

```elixir
defmodule MyAppWeb.PageController do
  use MyAppWeb, :controller

  # ...

  def show(conn, _params) do
    render(conn, "show.html")
  end
end
```

Lastly, add a template at `lib/myapp_web/controllers/page_html/show.html.heex`. The page's rendered markdown will be available in the `inner_content` assign:

```heex
<main>
  <%= @inner_content %>
</main>
```

That's it! Now try creating a file at `priv/pages/hello.md` and visiting `/hello`.

## Formatting

To prevent `mix format` from adding parenthesis to the `pages` macro similar to the other Phoenix Router macros, add `:phoenix_pages` to `.formatter.exs`:

```elixir
[
  import_deps: [:ecto, :ecto_sql, :phoenix, :phoenix_pages]
]
```

## Frontmatter

Frontmatter allows page-specific variables to be included at the top of a markdown file using the YAML format. If you're setting frontmatter variables (which is optional), they must be the first thing in the file and must be set between triple-dashed lines:

```markdown
---
title: Hello World
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
```

To specify which frontmatter values are expected in each page, set the `attrs` option:

```elixir
pages "/:page", PageController, :show,
  from: "priv/pages/**/*.md",
  attrs: [:title, author: nil]
```

Atom values will be considered required, and a compilation error will be thrown if missing from any of the pages. Key-values must come last in the list, and will be considered optional by defining a default value. Any frontmatter values not defined in the attributes list will be silently discarded.

Valid attribute values will be available in the assigns:

```heex
<main>
  <h1><%= @title %></h1>
  <h2 :if={@author}><%= @author %></h2>

  <%= @inner_content %>
</main>
```

## Syntax Highlighting

Phoenix Pages uses the [Makeup](https://github.com/elixir-makeup/makeup) project for syntax highlighting. To enable, import a theme listed below into your CSS bundle. The specifics of doing this highly depend on your CSS configuration, but a few examples are included below. In most cases, you will need to import `phoenix_pages/css/monokai.css` (or whatever theme you choose) into your bundle and ensure `deps` is included as a vendor directory.

<details>
  <summary><b>Themes</b></summary>

  <ul>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#abap">abap</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#algol">algol</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#algol_nu">algol_nu</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#arduino">arduino</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#autumn">autumn</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#black_white">black_white</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#borland">borland</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#colorful">colorful</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#default">default</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#emacs">emacs</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#friendly">friendly</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#fruity">fruity</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#igor">igor</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#lovelace">lovelace</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#manni">manni</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#monokai">monokai</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#murphy">murphy</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#native">native</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#paraiso_dark">paraiso_dark</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#paraiso_light">paraiso_light</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#pastie">pastie</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#perldoc">perldoc</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#rainbow_dash">rainbow_dash</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#rrt">rrt</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#samba">samba</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#tango">tango</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#trac">trac</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#vim">vim</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#visual_studio">visual_studio</a></li>
    <li><a href="https://elixir-makeup.github.io/makeup_demo/elixir.html#xcode">xcode</a></li>
  </ul>
</details>

Next, add a Makeup lexer for your specific language(s) to the project dependencies. Phoenix Pages will pick up the new dependency and start highlighting your code blocks without any further configuration. No lexers are included by default.

<details>
  <summary><b>Lexers</b></summary>

  <ul>
    <li><a href="https://github.com/elixir-makeup/makeup_c">C</a> - <code>`{:makeup_c, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_diff">Diff</a> - <code>`{:makeup_diff, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_elixir">Elixir</a> - <code>`{:makeup_elixir, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_erlang">Erlang</a> - <code>`{:makeup_erlang, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/Billzabob/makeup_graphql">GraphQL</a> - <code>`{:makeup_graphql, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_eex">(H)EEx</a> - <code>`{:makeup_eex, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_html">HTML</a> - <code>`{:makeup_html, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/maartenvanvliet/makeup_js">Javascript</a> - <code>`{:makeup_js, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/elixir-makeup/makeup_json">JSON</a> - <code>`{:makeup_json, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/dottorblaster/makeup_rust">Rust</a> - <code>`{:makeup_rust, "~> 0.0"}`</code></li>
    <li><a href="https://github.com/Billzabob/makeup_sql">SQL</a> - <code>`{:makeup_sql, "~> 0.0"}`</code></li>
  </ul>
</details>

If your language of choice isn't supported, consider [writing a new Makeup lexer](https://github.com/elixir-makeup/makeup/blob/master/CONTRIBUTING.md#writing-a-new-lexer) to contribute to the community. Otherwise, you can use a JS-based syntax highlighter such as [highlight.js](https://highlightjs.org) by setting `code_class_prefix: "language-"` and `syntax_highlighting: false` in [`render_options`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#pages/4-options).

### ESBuild Example

Using the [ESBuild installer](https://github.com/phoenixframework/esbuild), add the `env` option to `config/config.exs`:

```elixir
config :esbuild,
  version: "0.17.18",
  default: [
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)},
    args: ~w(--bundle --outdir=../priv/static/assets js/app.js)
  ]
```

Then in `app.js`:

```javascript
import "phoenix_pages/css/monokai.css";
```

### Sass Example

Using the [Sass installer](https://github.com/CargoSense/dart_sass), add the `--load-path` flag to `config/config.exs`:

```elixir
config :dart_sass,
  version: "1.62.0",
  default: [
    cd: Path.expand("../assets", __DIR__),
    args: ~w(--load-path=../deps css/app.scss ../priv/static/assets/app.css)
  ]
```

Then in `app.scss`:

```scss
@import "phoenix_pages/css/monokai";
```

### Tailwind Example

Install the `postcss-import` plugin as described [here](https://tailwindcss.com/docs/using-with-preprocessors#build-time-imports) and add the following to `assets/postcss.config.js`:

```javascript
module.exports = {
  plugins: {
    "postcss-import": {}
  }
}
```

Then in `app.css`:

```css
@import "../../deps/phoenix_pages/css/monokai";
```

## Index Pages

To create an index page with links to all the other pages, create a normal GET route and use the [`id`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#pages/4-options) option alongside [`get_pages/1`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#c:get_pages/1) and [`get_pages!/1`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#c:get_pages!/1):

```elixir
get "/blog", BlogController, :index

pages "/blog/:page", BlogController, :show,
  id: :blog,
  from: "priv/blog/**/*.md",
  attrs: [:title, :author, :date]
```

```elixir
defmodule MyAppWeb.BlogController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    pages = MyAppWeb.Router.get_pages!(:blog)

    conn
    |> assign(:pages, pages)
    |> render("index.html")
  end

  def show(conn, _params) do
    render(conn, "show.html")
  end
end
```

```heex
<.link :for={page <- @pages} navigate={page.path}>
  <%= page.assigns.title %>
</.link>
```

### Sorting

The pages returned from the `get_pages` functions will be sorted by filename. If you want to specify a different order during compilation rather than in the controller on every page load, use the [`sort`](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#pages/4-options) option:

```elixir
pages "/blog/:page", BlogController, :show,
  id: :blog,
  from: "priv/blog/**/*.md",
  attrs: [:title, :author, :date],
  sort: {:date, :desc}
```

Any attribute value from the frontmatter can be defined as the sort value.

## Defining Paths

When defining the pages path, the `:page` segment will be replaced for each generated page **during compilation** with the values derived from `**` and `*`. This is different than segments in regular routes, which are parsed **during runtime** into the `params` attribute of the controller function.

For example, let's say you have the following file structure:

```
├── priv/
│  ├── pages/
│  │  ├── foo.md
│  │  ├── bar/
│  │  │  ├── baz.md
```

Defining `pages "/:page", from: "priv/pages/**/*.md"` in your router will create two routes: `get "/foo"` and `get "/bar/baz"`. You can even put the `:page` segment somewhere else in the path, such as `/blog/:page`, and it will work as expected creating `get "/blog/foo"` and `get "/blog/bar/baz"`.

### Capture Groups

For complex scenarios, you have the option of using capture group variables instead of the `:page` segment.

Let's say you have the same file structure as above, but don't want the `baz` path to be nested under `/bar`. You could define `pages "/$2", from: "priv/pages/**/*.md"`, using `$2` instead of `:page`. This will create two routes: `get "/foo"` and `get "/bar"`.

Capture group variables will contain the value of the `**` and `*` chunks in order, starting at `$1`. Keep in mind that `**` will match all files and zero or more directories and subdirectories, and `*` will match any number of characters up to the end of the filename, the next dot, or the next slash.

For more info on the wildcard patterns, check out [Path.wildcard/2](https://hexdocs.pm/elixir/1.13/Path.html#wildcard/2).

## Extended Markdown

In addition to the customizable [markdown options](https://hexdocs.pm/phoenix_pages/PhoenixPages.html#pages/4-options), markdown rendering also supports IAL attributes by default. Meaning you can add HTML attributes to any block-level element using the syntax `{:attr}`.

For example, to create a rendered output of `<h1 class="foobar">Header</h1>`:

```markdown
# Header{:.foobar}
```

Attributes can be one of the following:

- `{:#id}` to define an ID
- `{:.className}` to define a class name
- `{:name=value}`, `{:name="value"}`, or `{:name='value'}` to define any other attribute

To define multiple attributes, separate them with spaces: `{:#id name=value}`.

## Local Development

If you add, remove, or change pages while running `mix phx.server`, they will automatically be replaced in the cache and you don't have to restart for them to take effect. To live reload when a page changes, add to the patterns list of the Endpoint config in `config/dev.exs`:

```elixir
config :myapp, MyAppWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/pages/.*(md)$",
      # ...
    ]
  ]
```