Render markdown articles in Rails

Written by
Juan Gallardo
Published
Last updated

Table of Contents

Previous: Set up a new Ruby on Rails project

Article 2 in Series: Build a blog with Ruby on Rails

Next: Set up Article resource

This guide explains my set-up for using Phlex components. However, Phlex components are just Plain Ruby Objects, and this object-oriented approach to the View layer of our MVC is extremely flexible. Feel free to modify this system to suit your needs

Install Phlex

See Phlex Beta website

  1. Install the Phlex gem. We want the beta version. bundle add phlex-rails --version=2.0.0.rc1

  2. Run the install generator bundle exec rails generate phlex:install

Pages, Components, and ApplicationLayout

Set up a Component base class

All our components will inherit from this class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /app/views/components/_base.rb

# frozen_string_literal: true

class Components::Base < Phlex::HTML
  include Phlex::Rails::Helpers::Routes
  include Phlex::Rails::Helpers::AssetPath
  include Phlex::Rails::Helpers::ImageTag
  include Phlex::Rails::Helpers::URLFor
  include InlineSvg::ActionView::Helpers

  if Rails.env.development?
    def before_template
      comment { "Before #{self.class.name}" }
      super
    end
  end
end

Set up a Page base class

This class inherits from our Component class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /app/views/pages/_base.rb
# frozen_string_literal: true

class Pages::Base < Components::Base
  PageInfo = Data.define(:title)
  
  def around_template
    render layout.new(page_info) do
      super
    end
  end
  
  def page_info
    PageInfo.new(
      title: page_title
    )
  end
end

Add a layout

Our pages can specify a layout. We only need one, but you’re free to write more

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# /app/views/layouts/application_layout.rb

class ApplicationLayout < Components::Base
include Phlex::Rails::Layout
include Phlex::Rails::Helpers::CurrentPage
include Phlex::Rails::Helpers::T

def initialize(page_info)
  @page_info = page_info
end

def view_template
  doctype

  html(lang: "en") do
    head do
      title {
        t("site.title") + " - " + @page_info.title
      }
      meta(
        name: "viewport",
        content: "width=device-width,initial-scale=1")
    end

    body {
      header do
        Navbar do |navbar|
          navbar.logo do
            inline_svg_tag("logo/logo_2.svg",
                           alt: "",
                           role: "img",
                           width: "200",
                           height: "auto")
          end

          navbar.links do
            a(href: root_path) { "Home" }
            a(href: articles_path) { "Blog" }
            a(href: about_path) { "About" }
            a(href: contact_path) { "Contact" }
          end
        end
      end

      main do
        yield
      end

      footer do
        p { "Footer" }
      end
    }
  end
end
end

Explanation: * Here you can see how HTML elements are rendered by passing a block to the element method with the content that should be rendered inside. * I have included a call to render a custom component Navbar which we will define below. * Inside this block we are using Navbars private links method. We determine the order these elements show up, but our component determines how to render its links method * We are also using a custom component Link which we will define below * Our page content is rendered inside main via yield

Auto-load components

Folder structure:

my_app_root/
├─ app/
│  ├─ views/
│  │  ├─ components/
│  │  │  ├─ link/
│  │  │  │  ├─ link.rb
│  │  │  ├─ navbar/
│  │  │  │  ├─ navbar.rb
│  │  │  ├─ _base.rb
│  │  ├─ layouts/
│  │  │  ├─ application_layout.rb
│  │  ├─ pages/
│  │  │  ├─ _base.rb
│  │  │  ├─ home.rb
│  │  │  ├─ about.rb
│  │  │  ├─ contact.rb

Pages and Components initializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# frozen_string_literal: true

module Pages
end

module Components
  extend Phlex::Kit
end

Rails.autoloaders.main.push_dir(
  "#{Rails.root}/app/views/pages",
  namespace: Pages
)

Rails.autoloaders.main.push_dir(
  "#{Rails.root}/app/views/components",
  namespace: Components
)
Rails
  .autoloaders
  .main
  .collapse(
    "#{Rails.root}/app/views/components/*"
  )

Explanation: * Push all files directly inside /app/views/pages into the Pages module * Push all files inside /app/views/components/ into the Components module, but look one level deep inside this directory

Modify autoload_paths

The following code fragment is simplified for convenience. Add the relevant lines to your file

1
2
3
4
5
6
7
8
9
10
11
12
# /config/application.rb

module App
  class Application < Rails::Application
    config
      .autoload_paths << "#{root}/app/views/layouts"
    config
      .autoload_paths << "#{root}/app/views/components"
    config
      .autoload_paths << "#{root}/app/views/pages"
  end
end

Pages

Set up a PagesController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /app/controllers/pages_controller.rb

class PagesController < ApplicationController
  def home
    render Pages::Home.new
  end
  
  def about
    render Pages::About.new
  end
  
  def contact
    render Pages::Contact.new
  end
end

Write some placeholder pages

1
2
3
4
5
6
7
8
9
10
11
# /app/views/pages/home.rb

class Pages::Home < Pages::Base
  def page_title = "Home"
  
  def layout = ApplicationLayout
  
  def view_template(&)
    h1 { "Home" }
  end
end

Do the same for other static pages

Your first custom components

1
2
3
4
5
6
7
8
9
10
11
12
13
# /app/view/components/navbar/navbar.rb

class Components::Navbar < Components::Base
  def view_template(&)
    div(class: "container") do
      div(class: "nav", &)
    end
  end

  def links(&)
    nav(class: "links", &)
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# /app/view/components/link/link.rb

class Components::Link < Components::Base
  def initialize(href:, is_disabled: false)
    @href        = href
    @is_disabled = is_disabled
    super()
  end
  
  def view_template(&)
    tag do
      yield     
    end
  end
  
  def tag(&)
    if @is_disabled
      span(class: [ ("link"),
                 ("link--disabled") ]) do
        yield
      end
    else
      a(class: "link", href: @href) do
        yield
      end
    end
  end
end

This should give you an idea of what’s possible with Phlex components. In our application layout we are initializing our Navbar component with Link instances. We are giving these links an is_disabled argument that is based on whether the current route matches the link target. The Link component encapsulates the business logic for rendering disabled links. Pretty cool, right?