Avo

An example about how to create a blog with both Maglev and Avo.

Avo is an improved and more polished alternative of the venerable ActiveAdmin gem. In short, you build a super clean back-office to manage the records of your application and just by running a few commands in the terminal.

If you don't want to go through all the steps, we set up a Git repository for this sample application.

Create a brand new Rails app

rails new weblog -j esbuild --css tailwind -d=postgresql
cd weblog

Add the extra gems:

gem "image_processing", "~> 1.2"

Install ActiveStorage

bin/rails active_storage:install

Install ActionText

bin/rails action_text:install

Add Avo

bin/rails app:template LOCATION='https://avohq.io/app-template'

Create your first resource

Create a post resource

bin/rails generate resource post title:string content:rich_text
app/models/post.rb
class Post < ApplicationRecord
  validates :title, presence: true
  
  has_rich_text :content

  has_one_attached :cover_photo
end

Update the database

bin/rails db:migrate

Generate the related Avo resource.

rails generate avo:resource post
app/avo/resources/post_resource.rb
class PostResource < Avo::BaseResource
  self.title = :id
  self.includes = []
  # self.search_query = -> do
  #   scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
  # end

  # add fields here
  field :id, as: :id
  field :title, as: :text, required: true
  field :content, as: :trix
  field :cover_photo, as: :file, is_image: true, link_to_resource: true
end
bin/rails s

Awesome, we can now create our posts! Fire your browser, hit http://localhost:3000/avo/resources/posts/new. Write some posts.

Installation of Maglev

Add the Maglev gem to your Gemfile:

Gemfile
gem 'maglevcms', '~> 1.1.7'

Install the files Maglev requires to work and create your site in DB.

bundle install
bin/rails g maglev:install
bin/rails maglev:create_site

Use our local TailwindCSS config

In the layout of the Maglev theme app/views/theme/layout.html.erb, change this line

app/views/theme/layout.html.erb
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp">

by this one:

app/views/theme/layout.html.erb
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">

Add the font-poppings class to the body tag.

app/views/theme/layout.html.erb
...
<body class="font-poppings">
...

Finally, modify your tailwindconfig.js file to register the new family font.

tailwindconfig.js
module.exports = {
  content: [
    './app/views/**/*.html.erb',
    './app/helpers/**/*.rb',
    './app/assets/stylesheets/**/*.css',
    './app/javascript/**/*.js'
  ],
  theme: {
    extend: {
      fontFamily: {
        'poppins': ['"Poppins", sans-serif'],
      },
    },
  },
}

Tweak Avo / Maglev UIs

In this chapter, we're going to improve a little bit the UX of the Avo and Maglev administration UIs by adding links between the 2 UIs.

Open the config/initializers/avo.rb file and look for the config.main_menu statement. Replace it by the following:

config/initializers/avo.rb
config.main_menu = -> {
  section "Dashboards", icon: "dashboards" do
    all_dashboards
  end

  section "Resources", icon: "resources" do
    all_resources
  end

  section "Content", icon: "heroicons/outline/document" do
    link_to "Pages", path: "/maglev/editor"
  end
}

Open the config/initializers/maglev.rb file and add the following lines:

config/initializers/maglev.rb
config.title = 'Weblog - Pages'
config.back_action = ->(site) { redirect_to main_app.avo_path }

Create an home page based on Maglev sections

First, add 2 new section categories in the theme.yml file

app/theme/theme.yml
# Please, do not change the id of the theme
id: "theme"

name: "Weblog theme"

description: "A couple of sections tailored for a blog"

section_categories:
- name: nav
- name: hero
- name: blog

# Properties of your theme such as the primary color, font name, ...etc.
style_settings: []

pages:
- title: "Home page"
  path:  "/index"

# List of CSS class names used by your library of icons (font awesome, remixicons, ...etc)
icons: []

First section: nav

bin/rails g maglev:section nav_01 \
--name="Nav #1" \
--category=nav \
--settings logo:image call_to_action:link block:nav_item:link:link

This section is a little bit special because we can have one single instance of it in the page and its content must be the same in all the pages of the site. Moreover, it must be located at the top of the page.

Thus, open the app/theme/sections/nav/nav_01.yml file and modify the definition of the nav section like the following:

app/theme/sections/nav/nav_01.yml
[...]
site_scoped: true
[...]
insert_button: false
[...]
insert_at: top
[...]
singleton: true
[...]
sample:
  settings:
    logo: "/theme/logo-placeholder.svg"
    call_to_action: { text: "Action", url: "#" }
  blocks: 
  - type: nav_item
    settings:
      link: { text: "Nav item", url: "#" }

See how it looks: http://localhost:3000/maglev/admin/sections/nav_01/preview_in_frame

It doesn't look like a navbar, let's change the template of the section here at app/views/theme/sections/nav/nav_01.html.erb and replace it with:

app/views/theme/sections/nav/nav_01.html.erb
<%= maglev_section.wrapper_tag.div class: 'py-4 md:py-6 px-6 md:px-0' do %>
  <div class="w-full md:max-w-4xl mx-auto flex justify-between items-center">
    <div class="flex items-center space-x-12">
      <%= link_to maglev_site_link do %>
        <%= maglev_section.setting_tag :logo, class: 'h-10' %>
      <% end %>
      <ul class="flex space-x-8">
        <% section.blocks.each do |maglev_block| %>
          <%= maglev_block.wrapper_tag.li do %>
            <%= maglev_block.setting_tag :link, class: 'hover:text-gray-700' %>
          <% end %>
        <% end %>
      </ul>
    </div>
    <%= maglev_section.setting_tag :call_to_action, class: 'block transition-all bg-orange-500 text-white rounded-full px-6 py-2 hover:scale-105' %>
  </div>
<% end %>

Note: Making our navigation section responsive is not part of this guide.

Second section: hero

bin/rails g maglev:section hero_01 \
--name="Hero #1" \
--category=hero \
--settings title:text background_image:image

See how it looks: http://localhost:3000/maglev/admin/sections/hero_01/preview_in_frame

It needs a little bit of styling. Open the app/views/theme/sections/hero/hero_01.html.erb file and replace the file with:

app/views/theme/sections/hero/hero_01.html.erb
<%= maglev_section.wrapper_tag.div class: 'py-6 md:py-12 px-6 md:px-0 bg-black/60 relative' do %>
  <%= maglev_section.setting_tag :background_image, class: 'absolute inset-0 object-cover h-full w-full mix-blend-overlay' %>
  <div class="container mx-auto">
    <div class="flex items-center justify-center flex-col min-h-[theme(spacing.80)] relative text-white text-center space-y-8">
      <%= maglev_section.setting_tag :title, html_tag: 'h1', class: 'text-4xl font-semibold leading-10' %>
    </div>
  </div>
<% end %>

Alright, it's looking good. Let's change the sample data before we see the section in action in the editor. Change the sample data at the bottom of the app/theme/sections/hero/hero_01.yml file:

app/theme/sections/hero/hero_01.yml
sample:
  settings:
    title: "Welcome to my blog!"
    background_image: "/theme/image-placeholder.jpg"
  blocks: []

Now, go to http://localhost:3000/maglev/admin/sections/hero_01/preview and click on the Take Screenshot button.

Third section: N latests posts

Generate the different files for the section

bin/rails g maglev:section latest_posts \
--category=blog \
--settings number_of_posts:select more_link:link

Tweak the definition of the section (app/theme/sections/blog/latest_posts.yml):

app/theme/sections/blog/latest_posts.yml
[...]

settings:
- label: "Number of posts"
  id: number_of_posts
  type: select
  options:
  - label: "Last 2 posts"
    value: "2"
  - label: "Last 3 posts"
    value: "3"
  - label: "Last 4 posts"
    value: "4"
  - label: "Last 5 posts"
    value: "5"
  default: "2"

[...]

sample:
  settings:
    number_of_posts: "2"
    more_link: 
      text: "More posts"
      url: "#"
  blocks: []

We're going to modify the HTML template of the section. Open the app/views/theme/sections/blog/latest_posts.html.erb file

app/views/theme/sections/blog/latest_posts.html.erb
<%= maglev_section.wrapper_tag.div class: 'py-3 md:py-6 px-6 md:px-0' do %>
  <div class="w-full md:max-w-4xl mx-auto">
    <div class="grid grid-cols-2 gap-8 border-y py-8 border-gray-200">
      <% Post.order(:created_at).limit(maglev_section.settings.number_of_posts.value.to_i).each do |post| %>
        <%= link_to main_app.post_path(post), class: 'block group' do %>
          <article class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-10">
            <div class="bg-gray-100 w-full md:w-32 md:min-w-[theme(spacing.32)] rounded-lg bg-transparent overflow-hidden relative">
              <%= image_tag main_app.url_for(post.cover_photo), class: 'object-cover' %>
            </div>
            <header class="space-y-1">
              <p class="text-gray-400 text-xs uppercase"><%= l post.created_at.to_date, format: :long %></p>
              <h2 class="text-gray-800 text-xl font-semibold group-hover:text-gray-600"><%= post.title %></h2>
            </header>
          </article>
        <% end %>
      <% end %>
    </div>
    <div class="flex justify-end mt-4">
      <div class="group hover:text-gray-600">
        <%= maglev_section.setting_tag :more_link %>
        &nbsp;→
      </div>
    </div>
  </div>
<% end %>

Fourth section: Highlighted post

First, we've to register the posts collection in Maglev. Open your config/initializers/maglev.rb file and add the following lines:

config/initializers/maglev.rb
config.collections = {
  products: {
    model: 'Post', # name of the ActiveRecord class
    fields: {
      label: :title,
      image: :thumbnail_url
    }
  }
}

Proceed by defining the thumbnail_url method in our Post model. Open the app/models/post.rb file and add:

app/models/post.rb
def thumbnail_url
  return nil unless cover_photo.attached?
  Rails.application.routes.url_helpers.rails_blob_url(cover_photo, disposition: 'attachment')
end

NOTE: You might have to add the following snippet code at the top of your config/environments/development.rb file.

Rails.application.default_url_options = { host: 'localhost', port: 3000 }

It's now time to generate the different files for the section.

bin/rails g maglev:section highlighted_post \
--category=blog \
--settings title post:collection_item:posts

Tweak the definition of the section (app/theme/sections/blog/latest_posts.yml):

app/theme/sections/blog/latest_posts.yml
sample:
  settings:
    title: "Highlighted"
    post: 
      id: first
  blocks: []

Note: Replace id: first with the id of an existing post if you want to preview/test your section against a specific post.

And lastly, replace the template of the section app/views/theme/blog/highlighted_post.html.erb by:

app/views/theme/blog/highlighted_post.html.erb
<%= maglev_section.wrapper_tag.div class: 'py-6 md:py-12 px-6 md:px-0' do %>
  <% post = maglev_section.settings.post.item %>
  
  <div class="w-full md:max-w-4xl mx-auto space-y-8">
    <h2 class="uppercase font-bold text-xl">
      <%= maglev_section.setting_tag :title %>
    </h2>

    <%= link_to main_app.post_path(post), class: 'block' do %>
      <article class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-10">
        <div class="bg-gray-100 w-full md:w-72 md:min-w-[theme(spacing.72)] rounded-lg bg-transparent overflow-hidden relative">
          <%= image_tag main_app.url_for(post.cover_photo), class: 'object-cover' %>
        </div>
        <div class="space-y-4">
          <header class="space-y-1">
            <p class="text-gray-400 text-xs uppercase"><%= l post.created_at.to_date, format: :long %></p>
            <h2 class="text-gray-800 text-2xl font-semibold"><%= post.title %></h2>
          </header>
          <div class="text-sm text-gray-700 leading-6">
            <%= truncate(strip_tags(post.content.to_s), length: 140) %>
          </div>
        </div>
      </article>
    <% end if post %>
  </div>
<% end %>

Template of a post

First, we are going to update the main layout of our Rails application. But before that, we have to update the app/controllers/application_controller.rb file like this:

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Maglev::StandaloneSectionsConcern
  before_action :fetch_maglev_site_scoped_sections
end

Then, change the app/views/layouts/application.html.erb with:

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Weblog</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

  <body class="bg-white font-poppins">
    <%= render_maglev_section :nav_01 %>
    <%= yield %>
  </body>
</html>

Finally, open the app/views/posts/show.html.erb.

app/views/posts/show.html.erb
<div class="py-6 md:py-12 px-6 md:px-0 relative">
  <div class="w-full md:max-w-4xl mx-auto space-y-8">
    <div class="space-y-1">
      <h1 class="text-2xl font-bold">
        <%= @post.title %>
      </h1>
      <p class="text-gray-400 text-xs uppercase">
        <%= l @post.created_at.to_date, format: :long %>
      </p>
    </div>

    <%= image_tag main_app.url_for(@post.cover_photo), class: 'object-cover mx-auto' %>
    
    <article class="prose lg:prose-lg max-w-full">
      <%= @post.content %>
    </article>
  </div>
</div>

Improvements

Although we've covered a lot of topics, a couple of core features is still missing in order to achieve a fully functional blog.

  • add an authentication engine for both Avo and Maglev

  • create a lot more sections: footer, subscribe, call to action, forms, ...etc

  • complete the HTML/ERB template to list all the posts (+ paginate the lists)

  • RSS feeds

  • Sitemap

Last updated