Updated on November 4, 2019
Normally I'd be the first one to argue against re-inventing the wheel. Why build something that you can already get for free? When it came up at Scripted I was against it. It seemed silly. WordPress is free and it's the best blogging platform on the Internet. You want to build your own WordPress? What a waste!
When it came time for Toofr to put a blog up and start accumulating some good SEO juice, I followed my own advice. I set up an account on WPEngine and, because I hate doing WordPress design customizations, I hired a developer on Upwork to do the styling for me. For a few months it was fine.
And then I redesigned Toofr and I had to hire him again to update the design. Then I changed the header navigation and changed the page and URL structure. I forgot to update the blog, so the links were broken. I hired him again to fix it. And then I did a major redesign of the app and the blog design stayed the same. I hate it when the blog looks different than the main app. That's when I started to think there must be a better way.
And in fact there is!
Note to self: Rails scaffolding is awesome
I just needed to get inspired. I read this article from Thoughtbot about how they decided to make a custom blog for a client. I'd read about Jekyll and Octopress, two rubygems that are supposed to be easy to use and give you a bunch of features out of the box. Like Thoughtbot, I too dismissed them, figuring maintenance would be too much of a hassle. And like them, I decided to build my own blog, reinvention rules be damned.
I quickly re-discovered is that Rails scaffolding is awesome. The first thing I did was scaffold up an Article model, controller, and view with these fields: title, subtitle, category, and text. The scaffold generator looks like this:
rails g scaffold Article title:string text:text meta:string subtitle:string category:string
I wanted to include an image so I used the paperclip generator to do that:
rails g paperclip articles image
Now I have all the components of my blog post. The form looks like this:
Notice that cool text field? I wanted to include rich text and have the styling look as much like Medium as possible. I'd read that markdown editors are much better to use than WYSIWIG HTML editors, so after doing a bit of research I settled on the simplemde editor. It's as good as they say it is. All I had to do was include this in my form.html.erb file:
<% content_for :javascripts do %>
<script src="https://cdn.jsdelivr.net/simplemde/1.11.2/simplemde.min.js"></script>
<script type="text/javascript">
var simplemde = new SimpleMDE({
autosave: {
enabled: true,
uniqueId: "<%= @article.id %>",
delay: 1000,
},
});
</script>
<% end %>
This editor also solves my image upload problem for the blog posts themselves. Instead of having to build my own media library to handle multiple images per post, I made a free imgur account and upload images there. I then use the markdown image tag in my posts. Super easy! As a small side benefit, the Toofr blog now has its own feed of images on imgur.
All of this only took a couple of hours to build. The next time I do it, as I intend to for my other web apps, it'll take me less than 30 minutes. I'm documenting these steps as much for myself as I am for anyone who's reading this!
Building the index and single post views
One thing that scaffolding doesn't do for you is make the posts pretty. To do that I found this clean, simple, and free blog design template. I added a blog.less module and included it in my application.less file. The blog.less file looks like this:
//
// Blog page
// --------------------------------------------------
.intro-header {
background-color: #777777;
background: url(/assets/blog-header.jpg) no-repeat center center;
background-attachment: scroll;
-webkit-background-size: cover;
-moz-background-size: cover;
background-size: cover;
-o-background-size: cover;
margin-bottom: 50px;
}
.intro-header .site-heading,
.intro-header .post-heading,
.intro-header .page-heading {
padding: 100px 0 50px;
color: white;
}
@media only screen and (min-width: 768px) {
.intro-header .site-heading,
.intro-header .post-heading,
.intro-header .page-heading {
padding: 100px 0;
}
}
.intro-header .site-heading,
.intro-header .page-heading {
text-align: center;
}
.intro-header .site-heading h1,
.intro-header .page-heading h1 {
margin-top: 0;
font-size: 50px;
}
.intro-header .site-heading .subheading,
.intro-header .page-heading .subheading {
font-size: 24px;
line-height: 1.1;
display: block;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 300;
margin: 10px 0 0;
}
@media only screen and (min-width: 768px) {
.intro-header .site-heading h1,
.intro-header .page-heading h1 {
font-size: 80px;
}
}
.intro-header .post-heading h1 {
font-size: 35px;
}
.intro-header .post-heading .subheading,
.intro-header .post-heading .meta {
line-height: 1.1;
display: block;
}
.intro-header .post-heading .subheading {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 24px;
margin: 10px 0 30px;
font-weight: 600;
}
.intro-header .post-heading .meta {
font-family: 'Lora', 'Times New Roman', serif;
font-style: italic;
font-weight: 300;
font-size: 20px;
}
.intro-header .post-heading .meta a {
color: white;
}
@media only screen and (min-width: 768px) {
.intro-header .post-heading h1 {
font-size: 55px;
}
.intro-header .post-heading .subheading {
font-size: 30px;
}
}
.post-preview > a {
color: #333333;
}
.post-preview > a:hover,
.post-preview > a:focus {
text-decoration: none;
color: #0085A1;
}
.post-preview > a > .post-title {
font-size: 30px;
margin-top: 30px;
margin-bottom: 10px;
}
.post-preview > a > .post-subtitle {
margin: 0;
font-weight: 300;
margin-bottom: 10px;
}
.post-preview > .post-meta {
color: #777777;
font-size: 18px;
font-style: italic;
margin-top: 0;
}
.post-preview > .post-meta > a {
text-decoration: none;
color: #333333;
}
.post-preview > .post-meta > a:hover,
.post-preview > .post-meta > a:focus {
color: #0085A1;
text-decoration: underline;
}
@media only screen and (min-width: 768px) {
.post-preview > a > .post-title {
font-size: 36px;
}
}
.section-heading {
font-size: 36px;
margin-top: 60px;
font-weight: 700;
}
.caption {
text-align: center;
font-size: 14px;
padding: 10px;
font-style: italic;
margin: 0;
display: block;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
And then my index.html.erb to show all the posts is simply:
<% content_for(:header) do %>
<header class="intro-header">
<div class="container" style="margin-top: 0px !important">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="site-heading">
<h1>The Toofr Sales Hacker Blog</h1>
<hr class="small">
<span class="subheading">Turning hot leads to cold cash since 2013!</span>
</div>
</div>
</div>
</div>
</header>
<% end %>
<% @articles.each do |article| %>
<div class="row">
<div class="col-lg-2 col-md-1 text-right">
<%= link_to article do %>
<%= image_tag article.image(:thumb), style: "margin-top: 30px;" %>
<% end %>
</div>
<div class="col-lg-8 col-md-10">
<div class="post-preview">
<%= link_to article do %>
<h2 class="post-title"><%= article.title %></h2>
<h3 class="post-subtitle"><%= article.subtitle %></h3>
<% end %>
</div>
<hr>
</div>
</div>
<% end %>
The single post view is:
<%= provide :title, "Toofr Blog | #{@article.title}" %>
<%= provide :meta_description, @article.meta %>
<% content_for(:header) do %>
<% if @current_user && @current_user.admin? %>
<div class="pull-right">
<%= link_to 'Edit', edit_article_path(@article), class: 'btn btn-primary btn-xs' %> <%= link_to 'Destroy', @article, class: 'btn btn-danger btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
</div>
<header class="intro-header">
<div class="container" style="margin-top: 0px !important">
<div class="row">
<div class="col-lg-4 col-md-3 text-right">
<%= image_tag @article.image(:medium), style: "margin-top: 130px;" %>
</div>
<div class="col-lg-6 col-md-6">
<div class="post-heading">
<h1><%= @article.title %></h1>
<hr class="small">
<span class="subheading"><%= @article.subtitle %></span>
</div>
</div>
</div>
</div>
</header>
<% end %>
<article>
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<%= markdown(@article.text) %>
</div>
</div>
</div>
</article>
And now we have a really nice looking landing page and blog post page! I'd add some screenshots but you're looking at it right now!
Milking the SEO benefit for all it's worth
Having lots of pages of content on your site is key. Meta descriptions make it even better. You might have noticed above that I built a meta description into the layout file and place it into each post with this line:
<%= provide :meta_description, @article.meta %>
But I wanted SEO-friendly URLs too. One of the nice things about WordPress is it makes it easy to also get the SEO benefit of verbose URLs. So rather than https://www.toofr.com/articles/10, I instead have https://www.toofr.com/articles/why-i-decided-to-roll-my-own-blog (try both links -- they go to the same place!) To do this, I used the Friendly ID gem.
Setting this up is as simple as adding another couple of lines on my model and running another migration to put the unique SEO slug into my Articles table. The name of the rubygem is FriendlyId and that documentation is all here.
Now I have content, meta descriptions, images, and SEO-friendly URLs. I've almost checked off all stuff I'm missing by not using WordPress.
Where do I go from here?
Well, I have these categories but I'm not doing anything with them.
CATEGORIES = ['Company Announcements', 'Sales Hacks', 'Toofr Tips', 'Product Releases']
I'll eventually want to add a little navigation to filter by category. What I may do instead is build a search field into the index page that will look at title, subtitle, category, and text. This is easy to set up since my database is postgres. pg_search is a very friendly gem that that does the heavy lifting for me. Again, it's just a few more lines to the model and view.
As I keep blogging I'll need to paginate the index. I'm probably a dozen posts away from needing that so I'll punt it until then. I know it's coming, though, and I'll probably use the kaminari rubygem to do it. With that, I think I'll be done with development on the blog and will instead focus on writing and marketing these posts!