
Streamlining Email Integration
Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …

In last decade, there has been a significant emphasis on single-page applications (SPA) and many modern frontend
frameworks encourage their use. However, we believe that although the SPA approach has its advantages, the amount of
additional complexity it introduces is not justified. Most of the time, when developing web applications, the goal is
to build the best possible product efficiently. At Mailsnag, we have chosen to utilize modern frontend frameworks like
Vue and Vuetify. However, instead of following the typical Vue + Router SPA approach, we have opted for a Multi-Page
Application (MPA) approach.
In this article, we will demonstrate how to set up a Rails 7
project with Vuetify 3
(and Vue 3
) using the MPA approach. We will also use Turbo Drive
to get the performance benefits of SPA without the added complexity.
You can access the project’s source code in our GitHub repository . Additionally, each section will include a link to the corresponding changes.
This guide was written using Ruby v3.2, Node.js v18, and Yarn v1.22. Please ensure that you have these tools
available in your development environment so that you can follow along.
To begin, install the rails 7 gem and create a new app named rails7-vuetify3. Since we won’t be using default
JavaScript bundler and the Asset Pipeline, we’ll instruct Rails to skip them during app creation. Feel free to customize
the app as needed, such as specifying database options. Additionally, we’ll add a simple blogs controller with index
and show pages, which will be used later to render our Vue pages.
$ gem install rails -v 7.0.5
$ rails new --skip-asset-pipeline --skip-javascript rails7-vuetify3
$ cd rails7-vuetify3
$ rails g controller blogs index show
Fix routes by replacing contents of config/routes.rb with:
Rails.application.routes.draw do
resources :blogs
# Defines the root path route ("/")
root "blogs#index"
end
Vuetify 3 is using vite
for dev server and building production assets. It has a
good support for rails through vite-rails
gem. Let’s
add it to our project:
$ bundle add vite_rails
$ bundle exec vite install
$ yarn install
Now that we have vite working with rails, we need to add and configure vuetify app. First, let’s add vuetify
package to our package.json and couple extra packages that will be used by vuetify and vite:
$ yarn add vue vuetify @mdi/js
$ yarn add -D vite-plugin-rails vite-plugin-vuetify @vitejs/plugin-vue
Since we will be using some of the vue syntax in our rails views, we will need to use vue with runtime compiler.
Take a look at vite.js.ts to see how that is achieved. We will also need to configure vite and rails to create
vuetify app. Take a look at changes needed below.
Replace vite.config.ts with:
import { defineConfig } from "vite";
import RailsPlugin from "vite-plugin-rails";
import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify";
export default defineConfig({
plugins: [RailsPlugin({ stimulus: false }), vue(), vuetify()],
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
},
extensions: [
".mjs",
".js",
".ts",
".jsx",
".tsx",
".json",
".vue",
".sass",
".scss",
".css",
".png",
".svg",
],
},
appType: "mpa",
clearScreen: false,
});
replace app/frontend/entrypoints/application.js with:
import { createApp } from "vue";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const app = createApp({});
app.use(vuetify);
app.component("App", App);
app.mount("#app");
replace app/views/layouts/application.html.erb with:
<!DOCTYPE html>
<html>
<head>
<title>Rails7Vuetify3</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= vite_client_tag %>
<%= vite_javascript_tag 'application' %>
</head>
<body>
<div id="app">
<app>
<%= yield %>
</app>
</div>
</body>
</html>
Let’s create vuetify plugin in app/frontend/plugins/vuetify.js:
import { createVuetify } from "vuetify";
import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
export default createVuetify({
icons: {
defaultSet: "mdi",
aliases,
sets: {
mdi,
},
},
});
<template>
<v-app id="vuetify-app">
<v-main>
<p>Vuetify 3 App Running 🍾🎉</p>
<slot></slot>
</v-main>
</v-app>
</template>
<script>
export default {};
</script>
Now if you refresh blogs#index route (http://0.0.0.0:3000/blogs) you should see Vuetify 3 App Running 🍾🎉
Now that we have vuetify app working in our rails views, we can start adding individual pages for Blog index and
show actions. For this we will create pages/blogs/index.vue and pages/blogs/show.vue Vue SFC components in our app/frontend directory.
Then we will reference these components in our rails views and pass data to it.
Create app/frontend/pages/blogs/index.vue component:
<template>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-text>
<v-table hover>
<template #default>
<thead>
<tr>
<th class="text-start" style="width: 70px;">ID</th>
<th class="text-start">Name</th>
</tr>
</thead>
<tbody>
<tr v-for="blog in blogs" :key="blog.id">
<td>
<code>{{ blog.id }}</code>
</td>
<td>
<v-btn variant="plain" :href="blogLink(blog)">
<v-icon start>{{ mdiPost }}</v-icon> {{ blog.name }}
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mdiPost } from "@mdi/js";
export default {
props: {
blogs: { type: Array, required: true },
},
data() {
return {
mdiPost,
};
},
methods: {
blogLink(blog) {
return `/blogs/${blog.id}`;
},
},
};
</script>
And app/frontend/pages/blogs/show.vue component:
<template>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-title>
<v-icon start>{{ mdiPost }}</v-icon>
Showing Blog {{ blog.id }}
</v-card-title>
<v-card-text> Blog contents go here: {{ blog.name }} </v-card-text>
<v-card-actions>
<v-btn variant="plain" :href="blogsLink()"> Back to Blogs </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mdiPost } from "@mdi/js";
export default {
props: {
blog: { type: Object, required: true },
},
data() {
return {
mdiPost,
};
},
methods: {
blogsLink() {
return `/blogs`;
},
},
};
</script>
Let’s register those components as async components so they are only loaded when rendered. Update app/frontend/entrypoints/application.js
with:
import { createApp, defineAsyncComponent } from "vue";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const BlogsIndex = defineAsyncComponent(() => import("~/pages/blogs/index"));
const BlogsShow = defineAsyncComponent(() => import("~/pages/blogs/show"));
const app = createApp({});
app.use(vuetify);
app.component("App", App);
app.component("BlogsIndex", BlogsIndex);
app.component("BlogsShow", BlogsShow);
app.mount("#app");
Now, lets add some content in our BlogsController and render components in erb view files.
Update app/controllers/blogs_controller.rb with:
class BlogsController < ApplicationController
def index
@blogs = [
{ id: 1, name: "My first blog"},
{ id: 2, name: "Another blog"}
]
end
def show
@blog = { id: params[:id], name: "Some random blog #{params[:id]}"}
end
end
Update app/views/blogs/index.html.erb with:
<blogs-index :blogs="<%= @blogs.to_json %>" />
And app/views/blogs/show.html.erb with:
<blogs-show :blog="<%= @blog.to_json %>" />
If you refresh toy blogs#index (http://0.0.0.0:3000/blogs) page, you should see our vue components rendering the
blogs table with links to navigate to individual blog pages.
Every time we navigate to a different page, we usually have to wait for the browser to fetch the necessary assets and
render the page. This process can cause some slowdown, even if we have effective caching settings in place. The browser
often needs to “recompile” or “run” these assets, further contributing to the delay. However, we can take advantage of
Turbo Drive
to circumvent the need for loading and “compiling” assets
with every page load. This approach leads to a faster and smoother user experience, resembling that of SPA.
First, let’s add required packages:
$ yarn add @hotwired/turbo-rails vue-turbolinks
Then update app/views/layouts/application.html.erb to use turbolinks by adding 'data-turbo-track': 'reload' to
vite_javascript_tag. Your vite_javascript_tag should like following:
<%= vite_javascript_tag 'application', 'data-turbo-track': 'reload' %>
Finally, update app/frontend/entrypoints/application.js to utilize Turbo Drive:
import * as Turbo from "@hotwired/turbo";
Turbo.start();
import { createApp, defineAsyncComponent } from "vue";
import TurbolinksAdapter from "vue-turbolinks";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const BlogsIndex = defineAsyncComponent(() => import("~/pages/blogs/index"));
const BlogsShow = defineAsyncComponent(() => import("~/pages/blogs/show"));
document.addEventListener("turbo:load", () => {
const app = createApp({});
app.use(TurbolinksAdapter);
app.use(vuetify);
app.component("App", App);
app.component("BlogsIndex", BlogsIndex);
app.component("BlogsShow", BlogsShow);
app.mount("#app");
});
Now if you inspect your networks tab in your browser’s dev tools, you will see that when navigating between pages,
related assets are only loaded once. You should also notice that rendering of the page is very fast compared to previous
experience.
We showed you how to set up brand new Rails 7 project with modern frontend framework. We also showed how to utilize Vue
components within your App’s erb view templates, keep your frontend MPA while having speed close to SPA. We hope this
will help you in your next project.
Until next time amigos 🙌

Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …

Introduction Emails have become an essential means of communication in both personal and professional settings. But have …
No credit card required to sign up. All paid plans come with 30 day cancellation and full refund.
Get Started for Free