A Simple Exercise Web App Powered by Backbone.js

Way Too Long; Did not Read

I wrote a Django application hosted on Heroku that you can find at Exercise-Library.com. It’s a single page web application that’s using Backbone.js to filter and render an exercise library.

True Story Follows

In my last post I wrote about the need to build a website exclusively for the purpose of driving traffic to the Workout Generator application we’ve been hustling on. The idea is that free content can help to attract users and hopefully drive them to a paid product.

It needed to be optimized both for search engines and for actual IRL people. On one hand, I’m trying to rank for obscure exercises on Google where competition might be easy, and in a shotgun blast of a few hundred exercises with their own pages, I’m sure to emerge victorious. On the other hand, it’s 2K15, and navigating a site by making multiple web requests is simply not hip anymore.

Making a Site Search Engine Friendly

Of the things that I know for sure in the obscure world of SEO, the things that I know for sure:

  • The URL helps rank for keywords
  • The title tag helps rank for keywords
  • The above the fold H1 tags help rank for keywords

Therefore, I wrote a really simple Django application where I render a page for every exercise I have stored in a simple json file.

You can see that I have a very simple Django setup with only 3 URL patterns and 3 corresponding view functions:

urlpatterns = patterns('',
    url(r'^$', views.home, name='home'),
    url(r'^exercise/(?P<exercise_name>[-\w]+)/', views.exercise, name="exercise"),
    url(r'^muscle/(?P<muscle_name>[-\w]+)/', views.muscle, name="muscle"),
)

But these functions end up creating several hundred crawlable web pages. From here, I can try adding write ups for each exercise over time.

You can see that there’s a dynamic URL hierarchy that emerges:

Screen Shot 2015-02-11 at 8.09.43 AM

But from a search engine or user perspective, it’s just static.

The homepage also links to every exercise:
Screen Shot 2015-02-11 at 8.12.29 AM
But the user will never actually see this page because a Backbone view renders and replaces that content.

Making the Site User Friendly

The content in place for the search engines is actually not intended for users in the least. Even the potential write ups may or may not have any value for the user. The actual use case that’s trying to be captured is that someone is just trying to learn about some exercises he or she could do given muscle group and equipment.

So I took the same data that was otherwise rendered by Django and instead just took its json representation and turned it into a Backbone collection. The result is a single page view that renders super fast and has HTML5 videos:

Screen Shot 2015-02-11 at 7.58.37 AM

So I have a simple backbone collection:

var Exercise = Backbone.Model.extend({
});

var ExerciseCollection = Backbone.Collection.extend({
    model: Exercise
});

Then server-side, I’m passing the json data directly to the HTML template:

def home(request):
    exercise_json = Exercise().as_json()

    JSContext = {
        'exercises': exercise_json
    }
    render_data = {
        "JSContext": json.dumps(JSContext)
    }
    return global_render_to_response("basic_navigation/search_engine_content.html", render_data)

(There’s actually more data being passed, but I stripped it down to convey what’s happening).

Then in the Django template I can pass the data to javascript:

<script type="text/javascript">
    window.JSContext = {{ JSContext | safe}};
</script>

Then I can instantiate the backbone collection:

    <script type="text/javascript">
    $(document).ready(function(){
        var exerciseCollection = new ExerciseCollection(JSContext.exercises);
        var el = $(".replace-area");
        var exerciseView = new ExerciseFilterView(el, exerciseCollection);
        exerciseView.render();
    });
    </script>

Then I can just start creating a standard backbone application:

var ExerciseFilterView = Backbone.View.extend({
    events: {
        "click input[type='checkbox']": "changeFilter",
        "click a": "linkClick"
    },
    initialize: function(el, collection){
        this.template = _.template($("#exercise-filter-view").html());
        this.$el = el;
        this.collection = collection;
        this.allEquipment = JSContext.equipment;
        this.allMuscleGroups = JSContext.muscle_groups;
        this.allExerciseTypes = JSContext.exercise_types;
        this.filter = new Filter();
        this.exerciseListView = new ExerciseListView(this.filter, this.collection);
    },
    linkClick: function(evt){
        var idString = evt.target.id;
        var splitArray = idString.split("_");
        if(splitArray[0] === "muscle"){
            var muscleId = parseInt(splitArray[1], 10);
            this.filter.set("muscleGroupId", muscleId);
            this.filter.trigger("change");
        }
    },
    changeFilter: function(){
        this.updateFilterFromView();
    },
     updateFilterFromView: function(){
         var equipmentIds = [];
         this.$(".equipment-check").filter(":checked").each(function(){
             var elId = this.id;
             equipmentIds.push(elId.split("_")[1]);
         });

         var exerciseTypeIds = [];
         this.$(".exercise-type-check").filter(":checked").each(function(){
             var elId = this.id;
             exerciseTypeIds.push(elId.split("_")[1]);
         });
         this.filter.set("equipmentIds", equipmentIds);
         this.filter.set("exerciseTypes", exerciseTypeIds);
         this.filter.trigger("change");
     },
    render: function(){
        var renderData = {
            equipments: this.allEquipment,
            exerciseTypes: this.allExerciseTypes,
            muscleGroups: this.allMuscleGroups
        };
        this.$el.html(this.template(renderData));

        this.$(".tab-inner-content").html(this.exerciseListView.render().el);

        this.updateFilterFromView();
        return this;
    }
});

Views are separate from models, and everything is loosely coupled. Views re-render from listening to changes in models.

The End