Building a Basic Website with S3 and JavaScript

If you've ready my previous posts on configuring Amazon S3 to host a static web application and configuring a CloudFront distribution to deliver application content, then you're going to fit right in with this discussion which focuses on how to use popular frameworks to make use of templating in a basic static website.

This article pulls heavily from this repo.

Building up the templates!

Quick example that references several templates in the body.

./index.html:

<!DOCTYPE html>
<html lang="en" ng-app="profileApp">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="description" content="">
        <link rel="shortcut icon" href="/img/favicon.png">
        <title>SiteName :: SiteSlogan</title>
        <link href="/css/bootstrap.css" rel="stylesheet">
        <link href="/css/font-awesome.css" rel="stylesheet">
        <link href="/css/themes/cosmo.css" rel="stylesheet">
    </head>
    <body>
        <!-- Include /app/templates/header.html -->
        <div class="container" ng-include src="'app/templates/header.html'"></div>
    &lt;!-- Announcement / Broadcast div --&gt;
    &lt;div class="container"&gt;&lt;div class="row"&gt;&lt;div class="col-lg-12" ng-include src="'app/templates/announcement.html'"&gt;&lt;/div&gt;&lt;/div&gt;&lt;hr /&gt;&lt;/div&gt;
    
    &lt;!-- Page Content --&gt;
    &lt;div class="container"&gt;
        &lt;div class="row"&gt;
            &lt;div class="col-lg-8"&gt;&lt;ng-view&gt;&lt;/ng-view&gt;&lt;/div&gt;
            &lt;div class="col-lg-4" ng-include src="'app/templates/sidebar.html'"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    
    &lt;!-- Include /app/templates/footer.html --&gt;
    &lt;div class="container" ng-include src="'app/templates/footer.html'"&gt;&lt;/div&gt;
    
    &lt;!-- Placed at the end of the document so the pages load faster --&gt;
    &lt;script src="https://code.jquery.com/jquery-1.10.2.min.js"&gt;&lt;/script&gt;
    &lt;script src="/js/bootstrap.js"&gt;&lt;/script&gt;
    &lt;script src="/js/angular.js"&gt;&lt;/script&gt;
    &lt;script src="/js/angular-route.js"&gt;&lt;/script&gt;
    &lt;script src="/js/angular-sanitize.js"&gt;&lt;/script&gt;
    &lt;script src="/app/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;

</html>

This pulls in templates from ./app/templates/*.html using the JavaScript libraries at the bottom of the body. In particular the ./app/app.js file provides routing logic for the path templates.

The AngularJS Routing Logic

In the displayed ./index.html above, you’ll have noticed towards the bottom of the file I load a several JavaScript libraries - including the one that does the logic for this small website, ./app/app.js.

./app/app.js:

var profileApp = angular.module(‘profileApp’, [‘ngRoute’,‘ngSanitize’,‘analytics’])
.config(['$routeProvider',
function($routeProvider) {
console.log($routeProvider);
// Routing info for what templates to load.
$routeProvider.
when('/editor', {
templateUrl: ‘app/templates/editor.html’
}).
when('/articles/:id', {
templateUrl: ‘app/templates/article.html’
}).
when('/articles', {
templateUrl: ‘app/templates/articles.html’
}).
when('/about', {
templateUrl: ‘app/templates/about.html’
}).
when('/', {
templateUrl: ‘app/templates/home.html’
}).
otherwise({
redirectTo: ‘/’
});
}],"$locationProvider","$location",[function () {
}])
.filter(‘reverse’, function() {
return function(items) {
// Inverse an array utility.
return items.slice().reverse();
};
})
.filter(‘searchArticles’, function() {
return function(items,search) {
var filtered = [];
if(!search){return items;}
angular.forEach(items, function(item) {
if(angular.lowercase(item.tags).indexOf(angular.lowercase(search))!==-1){
filtered.push(item);
}
if(angular.lowercase(item.title).indexOf(angular.lowercase(search))!==-1){
filtered.push(item);
}
});
return filtered.filter( onlyUnique );
};
});

profileApp.controller(‘articlesCtrl’, function ($scope,$http, $location) { $http.get('/app/articles/list.json').success(function(data) { var yyyy = new Date().getFullYear().toString(); var mm = (new Date().getMonth()+1).toString();
var dd = new Date().getDate().toString(); var today = yyyy + ‘-’ + (mm[1]?mm:“0”+mm[0]) + ‘-’ + (dd[1]?dd:“0”+dd[0]); $scope.today = today; var list = []; var flist = []; data.forEach(function(article) { if(article.date <= today){ list.push(article); } if(article.date > today){ flist.push(article); } }); $scope.list = list; $scope.flist = flist; $scope.search = $location.search().search; }); });

profileApp.controller(‘articleCtrl’, function ($scope,$http, $location) { $http.get('/app/articles/list.json').success(function(data) { $scope.article = data[$location.path().split('/')[2]-1]; $http.get(data[$location.path().split('/')[2]-1].location).success(function(content) { $scope.article.content = content; }); }); });

profileApp.controller(‘featuredCtrl’, function ($scope,$http,$filter) { $http.get('/app/articles/list.json').success(function(data) { var yyyy = new Date().getFullYear().toString(); var mm = (new Date().getMonth()+1).toString();
var dd = new Date().getDate().toString(); var today = yyyy + ‘-’ + (mm[1]?mm:“0”+mm[0]) + ‘-’ + (dd[1]?dd:“0”+dd[0]); var list = []; data.forEach(function(article) { if(article.date <= today){ list.push(article); } }); $scope.featured = list[list.length-1]; $http.get(list[list.length-1].location).success(function(content) { $scope.featured.content = $filter(‘limitTo’)(content, list[list.length-1].limit) + ‘…'; }); }); });

profileApp.controller(‘interestsCtrl’, function ($scope,$http) { $http.get('/app/articles/list.json’).success(function(data) { var list = []; data.forEach(function(article) { var tags = article.tags.split(", “); tags.forEach(function(tag) { list.push(tag); }); }); $scope.tags = list.filter( onlyUnique ).sort(); }); });

// utility functions function onlyUnique(value, index, self) { return self.indexOf(value) === index; }

// Tracking Additive //(function(angular) {

angular.module(‘analytics’, [‘ng’]).service(‘analytics’, [ ‘$rootScope’, ‘$window’, ‘$location’, function($rootScope, $window, $location) { var track = function() { $window._gaq.push(['_trackPageview', $location.path()]); }; $rootScope.$on('$viewContentLoaded', track); } ]);

//}(window.angular));

Loading blog posts “dynamically”!

In the displayed app.js file above you may have noticed this section:

profileApp.controller(‘articlesCtrl’, function ($scope,$http, $location) {
$http.get('/app/articles/list.json').success(function(data) {
var yyyy = new Date().getFullYear().toString();
var mm = (new Date().getMonth()+1).toString();    
var dd = new Date().getDate().toString(); var today = yyyy + ‘-’ + (mm[1]?mm:“0”+mm[0]) + ‘-’ + (dd[1]?dd:“0”+dd[0]); $scope.today = today; var list = []; var flist = []; data.forEach(function(article) { if(article.date <= today){ list.push(article); } if(article.date > today){ flist.push(article); } }); $scope.list = list; $scope.flist = flist; $scope.search = $location.search().search; }); });

This section makes a short HTTP GET call to retrieve a list of our blog posts or really any simulated “dynamic” data. The file is simple:

./app/articles/list.json:

[
{
“title”    : “Example Article 12345”,
“desc”     : “You know, an example!”,
“path”     : “/articles/1/”,
“limit”    : 600,
“tags”     : “Example”,
“location” : “/app/articles/Example_Article_12345.html”,
“date”     : “1990-01-01”
}
]

This can be used by a template to make a repeating list or build up content. Example:

    <div ng-controller=“articlesCtrl”>
<section>
<header>
<h3>Articles</h3>
</header>
<aside>
<p><input type=“text” name=“test” ng-model=“search” placeholder=“Filter by title, tag” class=“form-control”></p>
</aside>
<article ng-repeat=“article in list | orderBy: ‘date’:true | searchArticles: search”>
<h4><a href="{{ article.path }}">{{ article.title }}</a><br /><small>Published: {{ article.date }} | Tags: {{ article.tags }}</small></h4>
<p ng-bind-html=“article.desc”></p>
</article>
</section>
</div>