Websites in RMarkdown

Here I’m going to cover the basics using RMarkdown to create an entire website, with multiple pages and common elements like a navigation bar connecting them. These are the same tools that I used to build this website!

There are a couple of things to keep in mind. First the pros about using RMarkdown. You can very quickly create a website that stitches together a number of different RMarkdown page and connects them to each other with a navigation var. Being able to use Markdown really decreases the amount of time that it would take to build a page from scratch, and loads everything into an environment we’re very comfortable with.

But there are some cons to. So far at least, it is pretty difficult to build a website with a directory structure – everything must be on the same level. This creates some organizational hassles for you and might look a little strange to a user paying attention to the urls. I provide some advice for getting around this in the Advanced Application section but for the most part your website will be flat. Finally, you won’t be able to avoid writing some HTML, CSS, and Javascript if you want to do anything beyond the very basics.

Basic Web pages in RMarkdown

Setup

Sites are made with three core pieces.

  1. A home page called index.html, which you knit from index.Rmd. It must have this ame.

  2. A unified theme document for the website, which also controls the nav-bar, which must be called _site.yml.

  3. A cascading style sheet, which can take any name you specify in _site.yml.

After that we start adding a few more components which make it a website instead of just a page

  1. A few additional pages, otherwise you might just knit a single page, these can have any names.

  2. A few folders for supporting files, such as images and documents, these can have any names.

  3. I add an R file that executes all the necessary knitting code which I call exec.R.

  4. An optional file with HTML to place in the header of each page

  5. An optional file with HTML to place in the footer of each page

I go into more details on creating pages and what goes into _site.yml and exec.R below. Don’t be overwhelmed by the fact that there are eight pieces here, its all pretty straight forward.

Creating pages

Create pages seperately as you would for just a single page RMarkdown document. The only difference is that you can leave most of the formatting documents to the unified theme document, such as the theme, the css file, and text highlighting.

There are a few options you might want to specify page by page though:

  • tags: controls the metadata for your page and adding relevant information for each page will increase the chances that your page appears on Google search results.
  • toc: creates a table of contents, which I find useful for tutorial pages such as this one but do not want to appear on all pages, which is why I do not specify it in _site.yml. Additional options for toc include toc_float and toc_depth.

Counter-intuitively, you probably want to make the title for each page the same, because the title will determine the name that appear on the browser tab. This does not create a level 1 header, instead write those manually on the page using the # formatting.

Here’s what the options look like for this page.

---
title: "Vincent Bauer"
tags: []
output: 
  html_document: 
    toc: true
    toc_float: true
    toc_depth: 2
    highlight: pygments
---

_site.yml

So at this point you have a couple of pages of content but they are unconnected and don’t have a unified theme. We fix this with the _site.yml file, the main purpose of which is to create a nav bar. You could also leave out `_site.yml’ if you specified the formatting on each page individually and included code for a nav-bar in an HTML header file.

Where do you get this file? Just make a .txt file, copyin the code below, save it, and then change the name to _site.yml.

Here’s the _site.yml for my page. You’ll notice that most of it is taken up by specifications for the nav-bar. I list the names of the four major categories of my site: Home, Research, Data, and Teaching, and then also include links to my CV, Google scholar page, LinkedIn page, and Stanford page.

Buttons can either be text or icon and then redirect to either a relative or absolute link. I’m using icons from FontAwesome, which you can tell by the fa- prefix, but you can also easily specify icons from Bootstrap. Using these libraries avoids the need for me to create my own icons folder and creates a standardized look.

You can also change these to drop-down menus, as explained by the RMarkdown page, but I prefer the simpler button style.

name: "Vincent Bauer"
navbar:
  title: "Vincent Bauer"
  left:
    - text: "Home"
      href: index.html
    - text: "Research"
      href: research.html
    - text: "Data"
      href: data.html
    - text: "Teaching"
      href: teaching.html
    - text: "CV" 
      href: files/Bauer.pdf
  right:
    - icon: fa-envelope
      href: mailto:vbauer@stanford.edu
    - icon: fa-google
      href: https://scholar.google.com/citations?user=D7L2PoMAAAAJ&hl=en
    - icon: fa-linkedin
      href: https://www.linkedin.com/in/vincent-bauer-24596384
    - icon: fa-tree
      href: https://politicalscience.stanford.edu/people/vincent-bauer

output:
  html_document:
    theme: cosmo
    highlight: textmate
    include:
      after_body: _footer.html
      in_header: _header.html
    css: ../styles.css
    self_contained: false

Finally, notice that I’m including references to a footer and a header file. These are both pieces of HTML that I want knitr to paste into the HTML that it generates and they are both pretty boring. My footer is just my copyright stamp. This is pasted as the last lines of code in the body section of every page, as you can see far below.

<div id="footer">
<p>Copyright &copy; 2016,  Vincent Bauer,  All rights reserved.</p>
</div>

The important part of my header is the script that changes how my anchors work, which is explained more below.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta http-equiv="Content-Style-Type" content="text/css">
  <title></title>
  <meta name="Generator" content="Cocoa HTML Writer">
  <meta name="CocoaVersion" content="1504">
  <!-- this script changes the anchor position -->
  <!-- http://jsfiddle.net/ianclark001/rkocah23/ -->
<script>
(function(document, history, location) {
  var HISTORY_SUPPORT = !!(history && history.pushState);

  var anchorScrolls = {
    ANCHOR_REGEX: /^#[^ ]+$/,
    OFFSET_HEIGHT_PX: 65,

    /**
     * Establish events, and fix initial scroll position if a hash is provided.
     */
    init: function() {
      this.scrollToCurrent();
      $(window).on('hashchange', $.proxy(this, 'scrollToCurrent'));
      $('body').on('click', 'a', $.proxy(this, 'delegateAnchors'));
    },

    /**
     * Return the offset amount to deduct from the normal scroll position.
     * Modify as appropriate to allow for dynamic calculations
     */
    getFixedOffset: function() {
      return this.OFFSET_HEIGHT_PX;
    },

    /**
     * If the provided href is an anchor which resolves to an element on the
     * page, scroll to it.
     * @param  {String} href
     * @return {Boolean} - Was the href an anchor.
     */
    scrollIfAnchor: function(href, pushToHistory) {
      var match, anchorOffset;

      if(!this.ANCHOR_REGEX.test(href)) {
        return false;
      }

      match = document.getElementById(href.slice(1));

      if(match) {
        anchorOffset = $(match).offset().top - this.getFixedOffset();
        $('html, body').animate({ scrollTop: anchorOffset});

        // Add the state to history as-per normal anchor links
        if(HISTORY_SUPPORT && pushToHistory) {
          history.pushState({}, document.title, location.pathname + href);
        }
      }

      return !!match;
    },
    
    /**
     * Attempt to scroll to the current location's hash.
     */
    scrollToCurrent: function(e) { 
      if(this.scrollIfAnchor(window.location.hash) && e) {
        e.preventDefault();
      }
    },

    /**
     * If the click event's target was an anchor, fix the scroll position.
     */
    delegateAnchors: function(e) {
      var elem = e.target;

      if(this.scrollIfAnchor(elem.getAttribute('href'), true)) {
        e.preventDefault();
      }
    }
  };

    $(document).ready($.proxy(anchorScrolls, 'init'));
})(window.document, window.history, window.location);
</script>

</head>
<body>
</body>
</html>

exec.R

Now you have all the pieces of your website ready, but how do you actually knit it together?

If you just open any of your markdown documents and hit the Knit to HTML button you’ll get a fully functional page with a nav-bar that uses the _site.yml file for its formatting but once you have a lot of pages it can be a pain to do this manually. Also now you have a folder with both .Rmd and .html files, while all you really want on your server is the HTML.

Instead, you can knit a whole site at once using the render_site function in the rmarkdown library. If you’ve set your working directory to your website folder, this command will create a new folder called _site, knit all .Rmd files that do not start with _, move the resulting HTML files into the _site folder, and move any additional folders or files that do not start with _ into the _site folder.

You could just run this command from the command line but you can take advantage of a few additional benefits if you create a script that executes it. First, you can specifying knitr options that you want to apply to every page, such as echoing, cacheing, and figure sizes. I have noticed that the cache path is not honored by render_site and it will just cache it to your working directory. Second, you can create any R objects and load any libraries that you would like to use on multiple pages of your website. For example, I list my Plotly axis options in the exec.R file and can then just reference them later.

#Setup
library(rmarkdown)
library(knitr)
library(beepr)
library(dplyr)

wd <- "/Volumes/External HD/Website/rweb2"
setwd(wd)

knitr::opts_chunk$set(echo = TRUE, include = TRUE, cache=TRUE, fig.width = 8, fig.height=6, fig.align="center", background = c(.95,.95,.95),message=FALSE)

#Plotly
x <- y <- list('fixedrange'= TRUE)

#Execute
render_site()
beep()  #finished rendering

Advanced Applications

That’s really all you need to know to get started! Here I’ve compiled a few more advanced issues to help you tackle some complications.

Faking website organization

I mentioned before that you cannot actually create directories for your website – that means no vbauer.com/data/project1.html page but I can have vbauer.com/project1.html. But you can still guide people through your website by the way that you set up your links, as I set up with my Teaching pages. Under the hood, the website doesn’t know that the tutorials that my Teaching page links are to children pages, it thinks everything is at the same level.

The main implication is subtle but this means that the nav-bar would show nothing indented on my tutorials. But if you glance above, you’ll notice that Teaching is indented, how come? I added some Javascript to explicitly indent the Teaching nav-bar item, and you can just copy it from me.

Here’s how the Javascript works. The code executes when the document is ready – this means after all of the pieces have been found but before anything has been displayed on your page. It looks for an html node with a link to the main Teaching page. But this is the link itself, and not the physical button, so it moves up to the parent of that link, which is the nav-bar button, and then it changes the class of that button to include active, which is what the CSS is looking for to give the indented looking shading.

Just paste all of this code somewhere in your document, the location is irrelevant for this particular function but will matter for some others.

<!-- this code selects the navbar item -->
<script>
$(document).ready(function(){
    $('a[href="teaching.html"]').parent().addClass("active");
});
</script>

Actal Website organization

If you’re willing to put in some work work, it is possible to hack together some subdirectories. I’ll go through the code here and give you a function that should be pretty easy to use. The general steps are that you first render all of the pages to a flat directory, then programmatically move them into new directories, and edit the HTML to have the correct references to dependencies.

The following function performs all of the necessary steps automatically, with a few assumptions about how your RMarkdown website is put togther. It takes as inputs a string with the woriking directory, including the “/_site" folder, and a matrix with the names of the html files you want to move, the subdirectories you want to move them to, and what nav-bar links should be activated.

Here’s that the matrix input needs to look like, with examples:

##      name                    dir        navbar         
## [1,] "data-example.html"     "data"     "data.html"    
## [2,] "teaching-example.html" "teaching" "teaching.html"

And here’s the function:

matrix <- cbind(name, dir, navbar)
dir <- paste0(wd, "/_site")

subdirectory <- function(dir, matrix){
  
  for(i in 1:nrow(matrix)){
    cat("Moving ", matrix[i, "name"], " ... ")
    old <- paste0(dir, "/", matrix[i, "name"])
    new <- paste0(dir, "/", matrix[i, "dir"], "/", matrix[i, "name"])
      
    #create the new directory
    dir.create(paste0(dir, "/", matrix[i, "dir"]), recursive = TRUE, showWarnings = FALSE)
    
    #copy files
    file.copy(from=old, to=new, overwrite=TRUE)
    file.remove(old)
    
    #edit the html to look one folder lower for the dependencies
    html <- readLines(new)
    n <- sum(gregexpr("/", matrix[i, "dir"], fixed=TRUE)[[1]] > 0) + 1
    prefix <- paste0(rep("../", n), collapse="")
    html <- gsub("../site_libs", paste0(prefix, "../site_libs"), html, fixed=TRUE)
    html <- gsub("../styles.css", paste0(prefix, "../styles.css"), html, fixed=TRUE)
    
    #edit the html to fix the nav-bar, editing all links within the code section
    nav.start <- grep(pattern='    <div class=\"navbar-header\">', html)
    nav.end <- grep(pattern='      <ul class=\"nav navbar-nav navbar-right\">', html)
    html[nav.start:nav.end] <- gsub('href=\"', paste0('href=\"', prefix), html[nav.start:nav.end], fixed=TRUE)
    
    #edit the html to insert the nav bar selection code
    page.start <- grep(pattern="<h1>", html)
    chunk <- c("<!-- this code selects the navbar item -->",
    "<script>", 
    "  $(document).ready(function(){", 
    paste0("    $('a[href=", '"', prefix, matrix[i, "navbar"],'"', "]').parent().addClass('active');"), 
    "  });", 
    "</script>",
    "")
    html <- c(html[1:(page.start-1)], chunk, html[(page.start):length(html)])
    
    writeLines(html, new)
    cat("finished!\n")

  }
}

If you have any improvements for this function I would love to hear them.

I also tried using the lib_dir and css options in the Rmd headers to create flexible site organization but it seems like those are overwritten by the options in the _site.yml file. I could remove the _site.yml file and added all of those options to each page individually but then I would have to manually specify the relative paths to the navigation bar links, the site libraries, and the css for every page, which seems like a pain, especially because my function is working reasonable well at the moment.

Collapsing sections

The Bootstrap library, which is preloaded by RMarkdown, makes it very easy to create collapsable sections, all you have to do is add a few lines of code.

First, you need a section that will hide and unhide. Wrap the section with the following code, replacing uniqueID with a name that uniquely identifies this section.

<div class="collapsed" id="uniqueID">
This section will appear and disappear. 
</div>

Second, you need something to click on that will reveal the hidden section. Wrap something with the following code, replacing the uniqueID with the same name that you gave the disappearing section before.

<div class="expand collapsed" data-toggle="collapse" data-target="#uniqueID" aria-expanded="false" aria-controls="uniqueID">
This section will trigger the appearance of the other section
</div>

Under the hood, Bootstrap is just adding and removing class names, which are then connected to the visibility of the section. If you want the section to begin fully expanded and then become collapsed after clicking on it, just change class="collapsed" to class="collapse". If you want a new section to expand after you’ve collapsed another, add the id of the new section to the data-target variable, like this data-target="#uniqueID,#newID" and give it the "in" class.

For more details, see the Bootstrap page here.

Operating system specific code

It can also get a little distracting to display code for multiple operating systems on the same page so I wrote some code to guess the user’s operating system and then hide code chunks accordingly.

Copy and paste the following code into your document.

<script>
$(document).ready(function(){
    getOS();//run getOS on page ready
});

function getOS(){  //determines the current operating system
  var OSName="Other OS";
  if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
  if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
  //$('span#loading').text("" + OSName); //update a piece of text, not using this currently
  $('#selectOS').val(OSName).change();  //update the select box, this will trigger the selectOS function and then the hideOS function
}


function selectOS(){  //changes OS Name by combobox value
   sel = document.getElementById("selectOS");
   var OSName=sel.options[sel.selectedIndex].value;
   hideOS(OSName);
}

function hideOS(OSName){  //hides content based on operating system
  if(OSName == "Windows"){
     $('.detectPC').css('display','block');
     $('.detectMac').css('display','none');
  } else if (OSName == "MacOS"){
     $('.detectPC').css('display','none');
     $('.detectMac').css('display','block');
  } else if (OSName == "Other OS"){
     $('.detectPC').css('display','block');
     $('.detectMac').css('display','block');
  }
}

</script>

It consists of three functions and triggers when the document is ready.

  1. getOS: this function uses the value of the .appVersion property to determine what operating system a user is working on. It then saves this information into a variable called OSName. This variable is then used to update the combo box on the main page and updating the combo box subsequently triggers the hideOS script, which reads the operating system information and hides various code chunks. I have also written the code for it to update a piece of inline text, but am not using this code currently.

  2. selectOS: this function uses user input to set the value of the OSName variable and then triggers the hideOS function, passing along the operating system information. It takes the value of the combo-box that displays the operating system.

  3. hideOS: this function receives the OSName from selectOS and then either hides or displays sections of code accordingly. These sections are wrapped in

    with the classes given above.

I have also included a combo-box on the page which 1) displays what operating system my script thinks the computer is using and 2) allows the user to change the code that is displayed. The combo-box code looks like this, and you can just insert it inline with any other text. It triggers the selectOS function when changed.

<select id="selectOS" onchange="selectOS()" style="text-align-last: center;">
    <option value="Windows">PC</option>
    <option value="MacOS">Mac</option>
    <option value="Other OS">Other System</option>
</select>

Text-align-last style just centers these combo-box values for Chrome.

Adding HTML dependencies

Sometimes you want to change the dependencies that are loaded by RMarkdown. For example, Font Awesome added some icons that I wanted to use before the RMarkdown team updated the function to include this library. You need to make a new html dependency using the htmltools package, and then attach this dependency to part of your page, in this example I create a new DIV and attach the dependency there.

library(htmltools)
dep <- htmlDependency(
    name = "font-awesome",
    version = "4.7.0",
    #src = rmarkdown_system_file("rmd/h/font-awesome-4.7.0"),  #alternative way of specifying the folder location
    src = "/Library/Frameworks/R.framework/Versions/3.3/Resources/library/rmarkdown/rmd/h/font-awesome-4.7.0",
    stylesheet = "css/font-awesome.min.css"
  )
attachDependencies(div("test"), dep)

I think that once you attach it to any page that causes RMarkdown to move the necessary files into the site_lib folder and then you can access these resources by adding a normal link to the header file.

<link href="../site_libs/font-awesome-4.7.0/css/font-awesome.min.css" rel="stylesheet" />

Anchors with fixed size headers

If you use the default navigation bar, this will add a header to all of your pages, which is great. The problem is that the normal anchor functions that allow you to create links that jump within a page don’t know that you have a header so they will actually navigate you to a position such that the section title is actually hidden by the navigation bar, not ideal at all. But there’s an easy fix. If you’re using anchors within a page, just plop this javascript section into your header file so that it gets appended to every page. The function also happens to animate the scrolling nicely as well. I got the script from here: http://jsfiddle.net/ianclark001/rkocah23/. Notice the OFFSET_HEIGHT_PX variable, which you can change.

<!-- this script changes the anchor position -->
<!-- http://jsfiddle.net/ianclark001/rkocah23/ -->
<script>
(function(document, history, location) {
  var HISTORY_SUPPORT = !!(history && history.pushState);

  var anchorScrolls = {
    ANCHOR_REGEX: /^#[^ ]+$/,
    OFFSET_HEIGHT_PX: 50,

    /**
     * Establish events, and fix initial scroll position if a hash is provided.
     */
    init: function() {
      this.scrollToCurrent();
      $(window).on('hashchange', $.proxy(this, 'scrollToCurrent'));
      $('body').on('click', 'a', $.proxy(this, 'delegateAnchors'));
    },

    /**
     * Return the offset amount to deduct from the normal scroll position.
     * Modify as appropriate to allow for dynamic calculations
     */
    getFixedOffset: function() {
      return this.OFFSET_HEIGHT_PX;
    },

    /**
     * If the provided href is an anchor which resolves to an element on the
     * page, scroll to it.
     * @param  {String} href
     * @return {Boolean} - Was the href an anchor.
     */
    scrollIfAnchor: function(href, pushToHistory) {
      var match, anchorOffset;

      if(!this.ANCHOR_REGEX.test(href)) {
        return false;
      }

      match = document.getElementById(href.slice(1));

      if(match) {
        anchorOffset = $(match).offset().top - this.getFixedOffset();
        $('html, body').animate({ scrollTop: anchorOffset});

        // Add the state to history as-per normal anchor links
        if(HISTORY_SUPPORT && pushToHistory) {
          history.pushState({}, document.title, location.pathname + href);
        }
      }

      return !!match;
    },
    
    /**
     * Attempt to scroll to the current location's hash.
     */
    scrollToCurrent: function(e) { 
      if(this.scrollIfAnchor(window.location.hash) && e) {
        e.preventDefault();
      }
    },

    /**
     * If the click event's target was an anchor, fix the scroll position.
     */
    delegateAnchors: function(e) {
      var elem = e.target;

      if(this.scrollIfAnchor(elem.getAttribute('href'), true)) {
        e.preventDefault();
      }
    }
  };

    $(document).ready($.proxy(anchorScrolls, 'init'));
})(window.document, window.history, window.location);
</script>