Links
Comment on page

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