Bat Country Coding

A Technology Blog|


All Posts


Sharp Stone Codewerks

Abstretta.com

Bibliogrify.com

"Different Things Vary"


Github

LinkedIn

Quick-and-Easy Rails Help/FAQ System

February 14, 2017

Any web application needs a help or FAQ section. There has to be documentation. At the same time, you as the developer don’t want to spend a ton of time building this part of the code, or have to learn a whole new kind of syntax for your documents. Some FAQ gems rely on ActiveRecord but - seriously - how often does your documentation change? And when it changes, it’s usually in relation to a code push, right? So what I prefer is a flat-file approach and Markdown formatted documents. That keeps things simple and easy to maintain.

The Files

The implementation is pretty simple. One controller, 3 views, and an initializer - and add a new entry to config/routes.rb. Everything else is static document files which can be easily modified and re-arranged as your system grows or changes.

- root
  - app
    - controllers
      - cover_letters_controller.rb
    - views
      - helppages
        - _modal.html.erb
        - content.html.erb
        - index.html.erb
    - config
      - initializers
        - help_pages.rb
  - static
    - docs
      - section1
        - doc1.md
        - doc2.md
        - ...
      - section2
      - section3

config/initializers/openresume/help_pages.rb

The initializer sets up your document path and initializes the sections and pages in each section.

HELP_PATH = "static/docs"
HELPDIR = File.expand_path(HELP_PATH ) << "/" # don't miss adding "/"
HELP_SECTIONS = Dir[File.join(HELPDIR, '**')].map {|f| f.sub!(HELPDIR, "")}
HELP_PAGES = {}
HELP_SECTIONS.each do |s|
  HELP_PAGES[s] = Dir[File.join(HELPDIR + '/',  s, '**')].map {|f| f.sub!(HELPDIR, HELP_PATH )}
end

The Controller

The controller is pretty basic. The index method just loads a view which shows the sections and pages. Most of the work is done in the view. The content method simply loads the selected Markdown file and returns the rendered HTML over AJAX. Again, the goal here is simplicity and ease of maintenance.

class HelppagesController < ApplicationController

  # Show the index of all help/faq
  def index
  end

  # Render a specific page as plain AJAX/HTML
  def content
    md = File.read(File.expand_path(params[:p]))
    parser = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
    html = parser.render(md)
    render :plain => html
  end

  # Create a human readable page name from the file name.
  # '___' is used to indicate a question-mark (useful for FAQ)
  def self.format_page_name(s, p)
    p.gsub("#{HELP_PATH}#{s}/", "").gsub(".md", "").gsub("___", "?").gsub("_", " ").gsub(".md", "").split("/").last
  end

end

…/views/helppages/index.html.erb

The index view iterates over the HELP_SECTIONS and shows the list of documents in each. Each document link kicks out to showHelpModal() which loads the AJAX content into an modal popup. This example uses Semantic UI, but

<% title "Help" %>
<h1><%= yield(:title) %></h1>

<%= render :partial => "modal" %>

<div class="ui relaxed divided list">
<% HELP_SECTIONS.each do |s| -%>

  <div class="item"><i class="folder icon"></i><div class="content">

      <div class="header"><%= s.upcase.gsub("_", " ") %></div>

      <div class="ui relaxed divided list">
        <% HELP_PAGES[s].each do |p| -%>
          <div class="item"><i class="file blue icon"></i><div class="content">
            <%= link_to HelppagesController::format_page_name(s, p),  "javascript:showHelpModal('#{p}');" %>
          </div></div>
        <% end -%>
      </div>

  </div></div>

<% end -%>
</div>

Using Semantic UI will yield an index that looks like this:

The individual documents will appear as a modal pop-up window as defined below in _modal.html.erb.

…/views/helppages/_modal.html.erb

This view instantiates the Semantic UI modal popup and defines the JavaScript to load and trigger it. If you’d rather have individual pages load as a regular routes, then you can skip this and change the index view to handle links in that manner.

<div class="ui modal" id="helpModal" style="height:400px; max-height: 400px; overflow:auto;">
  <i class="close icon"></i>
  <div class="content">
    <div class="description" id="helpcontent">
    </div>
  </div>
</div>

<script type="text/javascript">
function showHelpModal(p) {
  $( '#helpcontent' ).load( "/helppages/content?p=" + p );
  $( '#helpModal' ).modal({
    offset: 0,
    inverted: true
  }).modal('show');
};
</script>

…/config/routes.rb

Finally, update your routes to access the controller and set up a default /help route.

get 'helppages/index'
get 'helppages/content'
get 'help' => 'helppages#index'

And there you have it. A complete help/faq system in about 100 lines of code. There may be more elegant ways to accomplish this, but for quickly generating internal documentation this will get the job done. All your documentation is in easy to edit Markdown files and you can re-arrange things by simply moving things between folders on the desktop and then pushing the code back to git.