diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/layouts/partials/head.html b/layouts/partials/head.html
index de8d4e583d0e18d535be1f6ba912cde94cf752f0..cf553cf13a227583ef473433c9bbfeeff997b783 100644
--- a/layouts/partials/head.html
+++ b/layouts/partials/head.html
@@ -142,7 +142,7 @@
 <script type="text/javascript" src="/javascripts/pseudocode.js"></script>
 <script type="text/javascript" src="/javascripts/jquery.js"></script>
 <script type="text/javascript" src="/javascripts/head.js"></script>
-<script type="text/javascript" src="/javascripts/gitlab-widgets/gitlab-widgets.js"></script>
+<script type="module" src="/javascripts/gitlab-widgets/gitlab-widgets.js"></script>
 <script type="text/javascript" >
 document.addEventListener("DOMContentLoaded", function() {
     var blocks = document.getElementsByClassName("pseudocode");
diff --git a/layouts/shortcodes/gitlab-overview.html b/layouts/shortcodes/gitlab-overview.html
index 8ad6799715a68fea5e4979ecfa8aaed138551103..837010ab82a467b1bc0a33a3d422178c16199b2b 100644
--- a/layouts/shortcodes/gitlab-overview.html
+++ b/layouts/shortcodes/gitlab-overview.html
@@ -15,7 +15,7 @@
 
   <!-- End of widget arguments -->
 
-  <h1 class="project-name"></h2>
+  <h1 class="project-name"></h1>
   <!--
   <h2 class="project-header">Overview</h2>
   -->
diff --git a/layouts/shortcodes/gitlab-single-version.html b/layouts/shortcodes/gitlab-single-version.html
new file mode 100644
index 0000000000000000000000000000000000000000..0d3cbc79d4d8d33748470e86998129629cb6b23e
--- /dev/null
+++ b/layouts/shortcodes/gitlab-single-version.html
@@ -0,0 +1,56 @@
+<div class="gitlab-single-version-container">
+  <!-- Adds widget arguments to DOM -->
+
+  {{ range $key, $value := $.Params }}
+    <input type="hidden" id={{ $key }} value={{ $value }}></input>
+  {{ end }}
+
+  {{ if isset .Params `stats` }}
+    <input type="hidden" id="stats" value={{ .Get "stats" }}></input>
+  {{ end }}
+
+  <!-- End of widget arguments -->
+
+  <h1 class="release-name">Test</h1>
+
+  <div class="gitlab-single-version-overview" class="gitlab-overview">
+
+    <table class="gitlab-milestone-progress">
+      <th>Release</th>
+      <th class="completion-tableheader">Completion</th>
+      <th class="due-date-tableheader">Due Date</th>
+      <th class="percentage-column">Percentage</th>
+      <tr class="gitlab-single-version-entry">
+        <td class="title"></td>
+        <td class="percentage-complete"></td>
+        <td class="due-date"></td>
+        <td class="progress-bar">
+          <progress></progress>
+        </td>
+      </tr>
+    </table>
+
+    <div class="gitlab-overview-footer">
+      <div class="footer-element">
+        <span class="label">Commits:</span>
+        <span id="commits"></span>
+      </div>
+      <div class="footer-element">
+        <span class="label">Tickets:</span>
+        <span id="issues">
+          <span id="issue-count"></span>
+          <span>/</span>
+          <span id="issue-total"></span>
+        </span>
+      </div>
+      <div class="footer-element">
+        <span class="label">MR:</span>
+        <span id="merge-requests">
+          <span id="mr-count"></span>
+          <span>/</span>
+          <span id="mr-total"></span>
+        </span>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/static/css/gitlab-widgets/gitlab-widgets.css b/static/css/gitlab-widgets/gitlab-widgets.css
index d3e748ca561b08c04b48727d66b9a411e2c5db76..6adcfa6a2677658d99d322a009b473f867257bc7 100644
--- a/static/css/gitlab-widgets/gitlab-widgets.css
+++ b/static/css/gitlab-widgets/gitlab-widgets.css
@@ -1,9 +1,17 @@
+/* Gitlab overview widget css */
+
 .project-name {
   font-size: 36px !important;
   text-align: center;
   background-color: #gray;
 }
 
+.release-name {
+  font-size: 36px !important;
+  text-align: center;
+  background-color: #gray;
+}
+
 .project-header {
   font-size: 32px !important;
   text-align: center;
@@ -18,7 +26,11 @@
   display: flex;
 }
 
-#gitlab-overview .label {
+.gitlab-overview .label {
+  font-weight: bold;
+}
+
+.footer-element .label {
   font-weight: bold;
 }
 
@@ -106,5 +118,34 @@
 }
 
 .red {
-  color: red;
+  color: red !important;
+}
+
+/* Gitlab single version css */
+.gitlab-single-version-overview {
+
+}
+
+.gitlab-milestone-progress {
+  width: 90%;
+  margin: auto;
+}
+
+.gitlab-milestone-progress .percentage-column {
+  width: 100%;
+}
+
+.gitlab-milestone-progress progress {
+  width: 100%;
+  margin: 0 0;
+}
+
+.gitlab-single-version-overview a {
+  width: 100%;
+  text-align: center;
+}
+
+.gitlab-single-version-entry {
+  width: auto;
+  margin: 0 0;
 }
diff --git a/static/javascripts/gitlab-widgets/gitlab-widgets.js b/static/javascripts/gitlab-widgets/gitlab-widgets.js
index a1ef8c51201fa0375628b40d08ceb6d5a813cb09..2c4dcac1b3a9cc5299d11787b3b707aa8b6c5b9a 100644
--- a/static/javascripts/gitlab-widgets/gitlab-widgets.js
+++ b/static/javascripts/gitlab-widgets/gitlab-widgets.js
@@ -8,50 +8,38 @@
  * {{< gitlab-overview servers="166:https://git.cleverthis.com/" v1.0="http://example2.com/" >}}
  */
 
-
-const GITLAB_CLEVERTHIS_TOKEN = "TOKEN_HERE"
-const GITLAB_QOTO_TOKEN = "TOKEN2_HERE"
-
-const PUBLIC_PROJECT_ENDPOINT = "/projects/"
-
-const ENDPOINTS = {
-    PROJECT: "/api/v4/projects/",
-
-    P: {
-        MILESTONES: "/milestones",
-        ISSUES: "/issues",
-        MERGE_REQUESTS: "/merge_requests",
-        COMMITS: "/repository/commits",
-    },
-    ISSUES: "/api/v4/issues",
-}
+import * as gitlabWidgets from "./helpers.js"
 
 const progressBarTitle = '<span class="releaseTitle"></span>';
 const progressBar = '<progress class="releaseProgressBar"></progress>';
 
 const issuesSearchParams = { scope: "all" };
 const issuesBugSearchParams = { scope: "all", labels: "bug" };
+const issuesOpenSearchParams = { scope: "all", state: "opened" };
 const issuesClosedSearchParams = { scope: "all", state: "closed" };
 
-function initOverviewWidgets() {
+function initWidgets() {
     for(var widget of $(".gitlab-overview-container")) {
-        initializeWidget(widget)
+        initializeOverviewWidget(widget)
+    }
+    for(var widget of $(".gitlab-single-version-container")) {
+        initializeSingleVersionWidget(widget)
     }
 }
 
-function initializeWidget(widget) {
+function initializeOverviewWidget(widget) {
     var overviewWidget = $(widget)
     var projectName = overviewWidget.find(".project-name")
     var releaseList = overviewWidget.find(".gitlab-overview-release-list")
     var progressList = overviewWidget.find(".gitlab-progress-list")
-    var projectId = getGitlabServerProjectID(overviewWidget, 0)
-    var displayCount = getDisplayCount(overviewWidget)
+    var projectId = gitlabWidgets.getGitlabServerProjectID(overviewWidget, 0)
+    var displayCount = gitlabWidgets.getDisplayCount(overviewWidget)
 
-    apiCall(buildProjectURL(overviewWidget, 0), (data) => {
+    gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(overviewWidget, 0), (data) => {
         projectName.text(data['path_with_namespace'])
     });
 
-    apiCall(buildProjectURL(overviewWidget, 0) + ENDPOINTS.P.MILESTONES, (data) => {
+    gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(overviewWidget, 0) + gitlabWidgets.ENDPOINTS.P.MILESTONES, (data) => {
 
         var displayedData = null
 
@@ -62,7 +50,7 @@ function initializeWidget(widget) {
         }
 
         displayedData.forEach(release => {
-            let releaseAnchor = generateMilestoneAnchor(release)
+            let releaseAnchor = gitlabWidgets.generateMilestoneAnchor(widget, release)
             releaseAnchor.appendTo(releaseList)
         })
 
@@ -70,15 +58,205 @@ function initializeWidget(widget) {
 
         populateProgressList(overviewWidget, progressList, data)
 
-        let displayStats = $('#stats')
+        let displayStats = overviewWidget.find('#stats')
         if(displayStats.length > 0 && displayStats[0].value == "true") {
             populateFooter(overviewWidget)
         } else {
-            removeFooter()
+            removeFooter(overviewWidget)
         }
     })
 }
 
+async function initializeSingleVersionWidget(widget) {
+    var singleVersionWidget = $(widget)
+    var releaseName = singleVersionWidget.find(".release-name")
+    var percentageComplete = singleVersionWidget.find(".gitlab-single-version-entry .percentage-complete")
+
+    var releaseData = null
+    var releaseAnchor = null
+
+    var addBugsField = false
+
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectMilestoneURL(widget, 0), (data) => {
+        releaseName.text(data['title'])
+        releaseAnchor = gitlabWidgets.generateMilestoneAnchor(widget, data)
+        let releaseNameDatum = $("<td></td>").append(releaseAnchor)
+
+        singleVersionWidget.find(".gitlab-single-version-entry .title").append(releaseAnchor)//text(data['title'])
+        singleVersionWidget.find(".gitlab-single-version-entry .due-date").text(data['due_date'])
+
+        releaseData = data
+    })
+
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectMilestoneURL(widget, 0), (data) => {
+        if(data['state'] == "active") {
+            initializeSingleVersionInProgress(widget, data)
+        } else if(data['state'] == "closed") {
+            addBugsField = true
+            initializeSingleVersionReleased(widget, releaseData)
+        }
+    })
+
+    let displayStats = singleVersionWidget.find('#stats')
+    if(displayStats.length > 0 && displayStats[0].value == "true") {
+        populateSingleVersionFooter(widget, releaseData['title'], addBugsField)
+    } else {
+        removeFooter(singleVersionWidget)
+    }
+}
+
+async function initializeSingleVersionReleased(widget, data) {
+    var singleVersionWidget = $(widget)
+    var title = singleVersionWidget.find(".title")
+    // DOM manipulations to get rid of fields that make no sense to display
+    singleVersionWidget.find(".percentage-column").remove()
+    singleVersionWidget.find(".progress-bar").remove()
+    singleVersionWidget.find(".due-date-tableheader").text("Release Date")
+    singleVersionWidget.find(".percentage-complete").text("Released")
+    singleVersionWidget.find(".completion-tableheader").css("width", "100%")
+
+    var milestoneBugIssuesParams = new URLSearchParams({ ...issuesSearchParams, milestone: data['title'], labels: ["Type::Bug"] })
+
+    // Color title based on whether bug issues exist
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, 0) + gitlabWidgets.ENDPOINTS.P.ISSUES + "?" + milestoneBugIssuesParams.toString(), (data) => {
+        if(data.length > 0) {
+            title.children("a").addClass("red")
+        } else {
+            title.children("a").addClass("green")
+        }
+    })
+
+
+}
+
+async function initializeSingleVersionInProgress(widget, data) {
+    var singleVersionWidget = $(widget)
+
+    // TODO: Calculate percentage complete
+    var allIssuesParams = new URLSearchParams({ ...issuesSearchParams, milestone: data['title'] })
+    var closedIssuesParams = new URLSearchParams({ ...issuesClosedSearchParams, milestone: data['title'] })
+
+    var percentageComplete = singleVersionWidget.find(".gitlab-single-version-entry .percentage-complete")
+
+    var allIssuesCount = 0
+    var closedIssuesCount = 0
+
+    for(let i = 0; i < gitlabWidgets.getGitlabServerCount(singleVersionWidget); i++) {
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + allIssuesParams.toString(), (allIssuesData) => {
+            allIssuesCount += allIssuesData.length
+        })
+
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + closedIssuesParams.toString(), (closedIssuesData) => {
+            closedIssuesCount += closedIssuesData.length
+        })
+    }
+
+    singleVersionWidget.find("progress").attr('max', allIssuesCount)
+    singleVersionWidget.find("progress").val(closedIssuesCount)
+
+    if(allIssuesCount == 0) {
+        percentageComplete.text("0%")
+    } else {
+        // TODO: Extract this to helper
+        let progressText = ((closedIssuesCount / allIssuesCount)*100).toFixed(1) + "%"
+        percentageComplete.text(progressText)
+    }
+
+    //populateSingleVersionFooter(widget, data['title'])
+}
+
+async function populateSingleVersionFooter(widget, milestoneTitle, addBugsField=false) {
+    //var milestoneId = $(widget).find("#milestone_id").val()
+
+    var commitCount = 0
+    var issueCount = 0
+    var issueTotal = 0
+    var mergeRequestCount = 0
+    var mergeRequestTotal = 0
+
+    var bugFooterElement = $('<span class="footer-element"></span>')
+
+    let issuesMilestoneSearchParams = new URLSearchParams({ ...issuesSearchParams, milestone: milestoneTitle })
+    let issuesOpenMilestoneSearchParams = new URLSearchParams({ ...issuesOpenSearchParams, milestone: milestoneTitle })
+
+    let openMRSearchParams = new URLSearchParams({ state: "opened", milestone: milestoneTitle })
+    let allMRSearchParams = new URLSearchParams({ state: "all", milestone: milestoneTitle })
+
+    for(let i = 0; i < gitlabWidgets.getGitlabServerCount(widget); i++) {
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + issuesMilestoneSearchParams.toString(), (data) => {
+            issueTotal += data.length 
+        })
+
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + issuesOpenMilestoneSearchParams.toString(), (data) => {
+            issueCount += data.length 
+        })
+
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.MERGE_REQUESTS + "?" + openMRSearchParams.toString(), (data) => {
+            mergeRequestCount += data.length
+        })
+
+        await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.MERGE_REQUESTS + "?" + allMRSearchParams.toString(), (data) => {
+            mergeRequestTotal += data.length
+        })
+    }
+
+    $(widget).find("#issue-count").text(issueCount)
+    $(widget).find("#issue-total").text(issueTotal)
+    $(widget).find("#mr-count").text(mergeRequestCount)
+    $(widget).find("#mr-total").text(mergeRequestTotal)
+
+    if(addBugsField == true) {
+        $('<span class="label">Bugs:</span>').appendTo(bugFooterElement)
+        $('<span id="bugs"></span>').appendTo(bugFooterElement)
+        $(widget).find(".gitlab-overview-footer").prepend(bugFooterElement)
+    }
+
+    var milestoneBugIssuesParams = new URLSearchParams({ ...issuesSearchParams, milestone: milestoneTitle, labels: ["Type::Bug"] })
+
+    // Color title based on whether bug issues exist
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, 0) + gitlabWidgets.ENDPOINTS.P.ISSUES + "?" + milestoneBugIssuesParams.toString(), (data) => {
+        getBugCountSinceRelease(widget, milestoneTitle).then((bugCount) => {
+            bugFooterElement.find("#bugs").text(bugCount)
+        })
+    })
+
+    let projectStatsSearchParams = new URLSearchParams({ statistics: "true" })
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, 0) + "?" + projectStatsSearchParams.toString(), (data) => {
+        $(widget).find("#commits").text(data['statistics']['commit_count'])
+    })
+}
+
+async function getBugCountSinceRelease(widget, milestoneTitle) {
+    var milestones = new Array()
+    var indice = 0
+    var bugCount = 0
+
+    let serverCount = gitlabWidgets.getGitlabServerCount(widget)
+
+    await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, 0) + gitlabWidgets.ENDPOINTS.P.MILESTONES, (data) => {
+        //console.log(data)
+        for(var milestone of data) {
+            milestones.push(milestone['title'])
+        }
+    })
+
+    milestones.sort(gitlabWidgets.compareSemanticVersions)
+    indice = milestones.indexOf(milestoneTitle)
+    var subsequentMilestones = milestones.slice(indice, milestones.length)
+
+    for(var milestone of subsequentMilestones) {
+        let milestoneBugIssuesParams = new URLSearchParams({ ...issuesSearchParams, milestone: milestone, labels: ["Type::Bug"] })
+        for(let i = 0; i < serverCount; i++) {
+            await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i) + gitlabWidgets.ENDPOINTS.P.ISSUES + "?" + milestoneBugIssuesParams.toString(), (data) => {
+                bugCount += data.length
+            })
+        }
+    }
+
+    return bugCount
+}
+
+/* Overview widget */
 async function populateFooter(widget) {
     var starCount = 0
     var issueCount = 0
@@ -87,33 +265,33 @@ async function populateFooter(widget) {
     var mergeRequestTotal = 0
     var commitCount = 0
 
-    for(let i = 0; i < getGitlabServerCount(widget); i++) {
+    for(let i = 0; i < gitlabWidgets.getGitlabServerCount(widget); i++) {
         // Project info
-        await apiCall(buildProjectURL(widget, i), (data) => {
+        await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i), (data) => {
             starCount += data['star_count']
             issueCount += data['open_issues_count']
         })
 
         // Issue count
-        await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.ISSUES, (data) => {
+        await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i) + gitlabWidgets.ENDPOINTS.P.ISSUES, (data) => {
             issueTotal += data.length
         })
 
         // Merge request count
         let openMRSearchParams = new URLSearchParams({ state: "opened" })
-        await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.MERGE_REQUESTS + "?" + openMRSearchParams.toString(), (data) => {
+        await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i) + gitlabWidgets.ENDPOINTS.P.MERGE_REQUESTS + "?" + openMRSearchParams.toString(), (data) => {
             mergeRequestCount += data.length
         })
 
         // Merge request count, part 2
         let allMRSearchParams = new URLSearchParams({ state: "all" })
-        await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.MERGE_REQUESTS + "?" + allMRSearchParams.toString(), (data) => {
+        await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i) + gitlabWidgets.ENDPOINTS.P.MERGE_REQUESTS + "?" + allMRSearchParams.toString(), (data) => {
             mergeRequestTotal += data.length
         })
 
         // Count the project commits
         let projectStatsSearchParams = new URLSearchParams({ statistics: "true" })
-        await apiCall(buildProjectURL(widget, i) + "?" + projectStatsSearchParams.toString(), (data) => {
+        await gitlabWidgets.apiCall(gitlabWidgets.buildProjectURL(widget, i) + "?" + projectStatsSearchParams.toString(), (data) => {
             // We don't sum these values because we don't want to double-count commits
             // We want the higher count, though
             let singleProjectCommitCount = data['statistics']['commit_count']
@@ -131,14 +309,14 @@ async function populateFooter(widget) {
     widget.find(".gitlab-overview-footer #commits").text(commitCount)
 }
 
-function removeFooter() {
-    $('.gitlab-overview-footer').remove()
+function removeFooter(widget) {
+    $(widget).find('.gitlab-overview-footer').remove()
 }
 
 function styleReleaseName(widget, releases) {
     Array.from(releases).forEach(release => {
         var allBugIssuesParams = new URLSearchParams({ ...issuesBugSearchParams, milestone: release.innerText })
-        apiCall(getGitlabServerURL(widget, 0) + ENDPOINTS.ISSUES + "?" + allBugIssuesParams.toString(), (data) => {
+        gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, 0) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + allBugIssuesParams.toString(), (data) => {
             if(data.length == 0) {
                 $(release).addClass("green")
             } else {
@@ -149,7 +327,7 @@ function styleReleaseName(widget, releases) {
 }
 
 async function populateProgressList(widget, progressList, releases) {
-    var displayCount = getDisplayCount(widget)
+    var displayCount = gitlabWidgets.getDisplayCount(widget)
 
     var displayedReleases = null
 
@@ -161,7 +339,7 @@ async function populateProgressList(widget, progressList, releases) {
 
 
     for(const release of displayedReleases) {
-        let releaseAnchor = generateMilestoneAnchor(release)
+        let releaseAnchor = gitlabWidgets.generateMilestoneAnchor(widget, release)
         let releaseNameDatum = $("<td></td>").append(releaseAnchor)
 
         var entry = $('<tr class="gitlabOverviewEntry"></div>').appendTo(progressList);
@@ -179,14 +357,14 @@ async function populateProgressList(widget, progressList, releases) {
         var allIssuesCount = 0
         var closedIssuesCount = 0
 
-        for(let i = 0; i < getGitlabServerCount(widget); i++) {
+        for(let i = 0; i < gitlabWidgets.getGitlabServerCount(widget); i++) {
 
             // Get issues of milestone_title, open and closed
-            await apiCall(getGitlabServerURL(widget, i) + ENDPOINTS.ISSUES + "?" + allIssuesParams.toString(), (allIssuesData) => {
+            await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + allIssuesParams.toString(), (allIssuesData) => {
                 allIssuesCount += allIssuesData.length
             });
 
-            await apiCall(getGitlabServerURL(widget, i) + ENDPOINTS.ISSUES + "?" + closedIssuesParams.toString(), (closedIssuesData) => {
+            await gitlabWidgets.apiCall(gitlabWidgets.getGitlabServerURL(widget, i) + gitlabWidgets.ENDPOINTS.ISSUES + "?" + closedIssuesParams.toString(), (closedIssuesData) => {
                 closedIssuesCount += closedIssuesData.length
             });
         }
@@ -217,73 +395,8 @@ async function populateProgressList(widget, progressList, releases) {
     }
 }
 
-/*
- * Helper functions
- *
- */
-
-function getDisplayCount(widget) {
-    var overviewWidget = $(widget)
-    var displayCount = null
-
-    if(overviewWidget.find('.displayCount').length > 0) {
-        displayCount = overviewWidget.find('.displayCount')[0].value
-    }
-
-    return displayCount
-}
-
-function getGitlabServerCount(widget) {
-    return widget.find("#servers").val().split(",").length
-}
-
-function getGitlabServerURLs() {
-    return $("#servers")
-}
-
-function getGitlabServerURL(widget, index) {
-    var overviewWidget = $(widget)
-    return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[1]
-}
-
-function getGitlabServerProjectID(widget, index) {
-    var overviewWidget = $(widget)
-    return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[0]
-}
-
-function generateMilestoneAnchor(release) {
-    let override = $("#" + $.escapeSelector(release.title))
-    if (override.length > 0) {
-        return $("<a href='" + override.val() + "'>" + release.title + "</a>")
-    } else {
-        return $("<a href='" + release['web_url'] + "'>" + release.title + "</a>")
-    }
-}
-
-function buildProjectURL(widget, index) {
-    const endpoint = ENDPOINTS.PROJECT + getGitlabServerProjectID(widget, index)
-    var url = new URL(endpoint, getGitlabServerURL(widget, index))
-    return url
-}
-
-function apiCall(endpoint, callback) {
-    var token = null
-    if(endpoint.toString().includes("cleverthis.com")) {
-        token = GITLAB_CLEVERTHIS_TOKEN
-    } else if(endpoint.toString().includes("qoto.org")) {
-        token = GITLAB_QOTO_TOKEN
-    } else {
-        return
-    }
-
-    return $.ajax({
-        url: endpoint,
-        dataType: 'json',
-        headers: { "PRIVATE-TOKEN": token },
-    }).done(callback)
-}
 
 // Only initialize widgets when page is done loading
 $(document).ready(() => {
-    initOverviewWidgets()
+    initWidgets()
 })
diff --git a/static/javascripts/gitlab-widgets/helpers.js b/static/javascripts/gitlab-widgets/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ea587950d73f4ee18f5f8ab1b52fb0486f8d6d9
--- /dev/null
+++ b/static/javascripts/gitlab-widgets/helpers.js
@@ -0,0 +1,113 @@
+export { getDisplayCount, getGitlabServerCount, getGitlabServerURLs, getGitlabServerURL,
+         getGitlabServerProjectID, generateMilestoneAnchor, buildProjectURL, buildProjectMilestoneURL, 
+         apiCall, compareSemanticVersions, ENDPOINTS, PUBLIC_PROJECT_ENDPOINT };
+
+const GITLAB_CLEVERTHIS_TOKEN = "glpat--dYnjsqjXfHxkf6yrzvL"
+const GITLAB_QOTO_TOKEN = "glpat-7LzfVvDzWyb_8gwgpG2b"
+
+const PUBLIC_PROJECT_ENDPOINT = "/projects/"
+
+const ENDPOINTS = {
+    PROJECT: "/api/v4/projects/",
+
+    P: {
+        MILESTONES: "/milestones/",
+        ISSUES: "/issues",
+        MERGE_REQUESTS: "/merge_requests",
+        COMMITS: "/repository/commits",
+    },
+    MERGE_REQUESTS: "/api/v4/merge_requests",
+    ISSUES: "/api/v4/issues",
+}
+
+/*
+ * Helper functions
+ *
+ */
+
+function getDisplayCount(widget) {
+    var overviewWidget = $(widget)
+    var displayCount = null
+
+    if(overviewWidget.find('.displayCount').length > 0) {
+        displayCount = overviewWidget.find('.displayCount')[0].value
+    }
+
+    return displayCount
+}
+
+function getGitlabServerCount(widget) {
+    return $(widget).find("#servers").val().split(",").length
+}
+
+function getGitlabServerURLs() {
+    return $("#servers")
+}
+
+function getGitlabServerURL(widget, index) {
+    var overviewWidget = $(widget)
+    return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[1]
+}
+
+function getGitlabServerProjectID(widget, index) {
+    var overviewWidget = $(widget)
+    return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[0]
+}
+
+function generateMilestoneAnchor(widget, release) {
+    let override = $(widget).find("#" + $.escapeSelector(release.title))
+    if (override.length > 0) {
+        return $("<a href='" + override.val() + "'>" + release.title + "</a>")
+    } else {
+        return $("<a href='" + release['web_url'] + "'>" + release.title + "</a>")
+    }
+}
+
+function buildProjectURL(widget, index) {
+    const endpoint = ENDPOINTS.PROJECT + getGitlabServerProjectID(widget, index)
+    var url = new URL(endpoint, getGitlabServerURL(widget, index))
+    return url
+}
+
+function buildProjectMilestoneURL(widget, index) {
+    const milestone_id = $(widget).find("#milestone_id").val()
+    const endpoint = ENDPOINTS.PROJECT + getGitlabServerProjectID(widget, index) + ENDPOINTS.P.MILESTONES + milestone_id
+    var url = new URL(endpoint, getGitlabServerURL(widget, index))
+    return url
+}
+
+function apiCall(endpoint, callback) {
+    var token = null
+    if(endpoint.toString().includes("cleverthis.com")) {
+        token = GITLAB_CLEVERTHIS_TOKEN
+    } else if(endpoint.toString().includes("qoto.org")) {
+        token = GITLAB_QOTO_TOKEN
+    } else {
+        return
+    }
+
+    return $.ajax({
+        url: endpoint,
+        dataType: 'json',
+        headers: { "PRIVATE-TOKEN": token },
+    }).done(callback)
+}
+
+const compareSemanticVersions = (a, b) => {
+ 
+    // 1. Split the strings into their parts.
+    const a1 = a.split('.');
+    const b1 = b.split('.');    // 2. Contingency in case there's a 4th or 5th version
+    const len = Math.min(a1.length, b1.length);    // 3. Look through each version number and compare.
+    for (let i = 0; i < len; i++) {
+        const a2 = +a1[ i ] || 0;
+        const b2 = +b1[ i ] || 0;
+        
+        if (a2 !== b2) {
+            return a2 > b2 ? 1 : -1;        
+        }    }
+    
+    // 4. We hit this if the all checked versions so far are equal
+    //
+    return b1.length - a1.length;
+};