Integrations
Avo
Step-by-step guide to build a blog with both Maglev and Avo.
Avo is a polished admin framework for Rails, often considered a modern alternative to ActiveAdmin. It helps you build a clean back-office to manage your app records with just a few terminal commands.
If you prefer to skip the full walkthrough, use this ready-made sample repository: maglevhq/avo-weblog.
Create a new Rails app
rails new weblog -j esbuild --css tailwind -d=postgresql
cd weblog
Add the extra gems:
gem "image_processing", "~> 1.2"
Install Active Storage
bin/rails active_storage:install
Install Action Text
bin/rails action_text:install
Add Avo
bin/rails app:template LOCATION='https://avohq.io/app-template'
Create your first resource
Generate 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
Migrate the database
bin/rails db:migrate
Generate the corresponding 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
You can now create posts. Open http://localhost:3000/avo/resources/posts/new and create a few records.
Install Maglev
Add the Maglev gem to your Gemfile:
Gemfile
gem 'maglevcms', '~> 1.1.7'
Install Maglev files and create the initial site record in the database.
bundle install
bin/rails g maglev:install
bin/rails maglev:create_site
Use a local Tailwind CSS config
In your Maglev theme layout (app/views/theme/layout.html.erb), replace this line:
app/views/theme/layout.html.erb
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp">
with:
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-poppins class to the <body> tag.
app/views/theme/layout.html.erb
... <body class="font-poppings"> ...
Finally, update tailwindconfig.js to register the new font family.
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'], }, }, }, }
Connect Avo and Maglev UIs
In this section, we improve navigation between the Avo and Maglev admin interfaces by adding links in both directions.
Avo UI: add a link to Maglev
Open config/initializers/avo.rb, find config.main_menu, and replace it with:
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 }
Maglev UI: add a link back to Avo
Open config/initializers/maglev.rb and add:
config/initializers/maglev.rb
config.title = 'Weblog - Pages' config.back_action = ->(site) { redirect_to main_app.avo_path }
Create a home page with Maglev sections
First, add two section categories to theme.yml:
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 special: there should be only one instance per page, the content should be shared across all pages, and it should appear at the top.
Then open app/theme/sections/nav/nav_01.yml and update the section definition:
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 still needs styling. Replace app/views/theme/sections/nav/nav_01.html.erb 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 %>
Now update the sample data before opening the section in the editor. Edit the sample block at the bottom of app/theme/sections/hero/hero_01.yml:
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.
This admin preview and Take Screenshot flow belongs to the Avo / theme admin integration. Maglev v3 does not include that UI; add JPEGs under public/theme/<category>/<section_id>.jpg yourself, as described in Section thumbnail.
Third section: Latest posts
Generate the section files:
bin/rails g maglev:section latest_posts \
--category=blog \
--settings number_of_posts:select more_link:link
Update the section definition (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: []
Now modify the section template at app/views/theme/sections/blog/latest_posts.html.erb:
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 %> → </div> </div> </div> <% end %>
Fourth section: Highlighted post
First, register the posts collection in Maglev. Open config/initializers/maglev.rb and add:
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 may need to add this snippet near the top of config/environments/development.rb.
Rails.application.default_url_options = { host: 'localhost', port: 3000 }
Now generate the section files:
bin/rails g maglev:section highlighted_post \
--category=blog \
--settings title post:collection_item:posts
Update the section definition (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.
Finally, replace app/views/theme/blog/highlighted_post.html.erb with:
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 %>
Post template
First, update the main app controller to load site-scoped Maglev sections:
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base include Maglev::StandaloneSectionsConcern before_action :fetch_maglev_site_scoped_sections end
Then replace 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 this guide covers many topics, a few core features are still missing for a fully production-ready blog.
- add an authentication engine for both Avo and Maglev
- create 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