diff --git a/README.md b/README.md
index 3be94d0c2bdcae0c22e37d1c06cfb97075c6bf3d..0799ad78e1d4014da493d0fda6599dc0c9ec2148 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,20 @@
-*NOTE: This README reflects the in development version of Mirador 3. For previous versions, please reference that release's README directly. Latest 2.x release: [v.2.7.0](https://github.com/ProjectMirador/mirador/tree/v2.7.0)*
+*NOTE: This README reflects the latest version of Mirador, Mirador 3. For previous versions, please reference that release's README directly. Latest 2.x release: [v.2.7.0](https://github.com/ProjectMirador/mirador/tree/v2.7.0)*
 # Mirador
-[![Build Status](https://travis-ci.com/ProjectMirador/mirador.svg?branch=master)](https://travis-ci.com/ProjectMirador/mirador) [![codecov](https://codecov.io/gh/ProjectMirador/mirador/branch/master/graph/badge.svg)](https://codecov.io/gh/ProjectMirador/mirador) 
+![Node.js CI](https://github.com/ProjectMirador/mirador/workflows/Node.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/ProjectMirador/mirador/branch/master/graph/badge.svg)](https://codecov.io/gh/ProjectMirador/mirador) 
 
 ## For Mirador Users
 You can quickly use and configure Mirador by remixing the [mirador-start](https://mirador-start.glitch.me/) Glitch.
 
+We recommend installing Mirador using a JavaScript package manager like [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/).
+
+```sh
+$ npm install mirador 
+
+# or
+
+$ yarn add mirador
+```
+
 If you are interested in integrating Mirador with plugins into your project, we recommend using webpack or parcel to integrate the es version of the packages. Examples are here:
 
 [https://github.com/ProjectMirador/mirador-integration](https://github.com/ProjectMirador/mirador-integration)
@@ -12,13 +22,13 @@ If you are interested in integrating Mirador with plugins into your project, we
 ## Adding translations to Mirador
 For help with adding a translation, see [src/locales/README.md](src/locales/README.md)
 
-## Running Mirador locally
+## Running Mirador locally for development
 
 Mirador local development requires [nodejs](https://nodejs.org/en/download/) to be installed.
 
 1. Run `npm install` to install the dependencies.
 
-## Starting the project
+### Starting the project
 
 ```sh
 $ npm start
diff --git a/src/components/CanvasLayers.js b/src/components/CanvasLayers.js
index c58a0eab8f7dd214dd68379f46e37616deaa8f40..3058725144710ac1240ba79139e02abec5aadff9 100644
--- a/src/components/CanvasLayers.js
+++ b/src/components/CanvasLayers.js
@@ -48,6 +48,19 @@ export class CanvasLayers extends Component {
     this.moveToTop = this.moveToTop.bind(this);
   }
 
+  /** */
+  handleOpacityChange(layerId, value) {
+    const {
+      canvasId, updateLayers, windowId,
+    } = this.props;
+
+    const payload = {
+      [layerId]: { opacity: value / 100.0 },
+    };
+
+    updateLayers(windowId, canvasId, payload);
+  }
+
   /** */
   onDragEnd(result) {
     const {
@@ -100,19 +113,6 @@ export class CanvasLayers extends Component {
     updateLayers(windowId, canvasId, payload);
   }
 
-  /** */
-  handleOpacityChange(layerId, value) {
-    const {
-      canvasId, updateLayers, windowId,
-    } = this.props;
-
-    const payload = {
-      [layerId]: { opacity: value / 100.0 },
-    };
-
-    updateLayers(windowId, canvasId, payload);
-  }
-
   /** @private */
   renderLayer(resource, index) {
     const {
diff --git a/src/components/IIIFThumbnail.js b/src/components/IIIFThumbnail.js
index a22c153a373529256747a16923356ba68943a06e..174e9af0bb922b4441b194df073241420c1f22db 100644
--- a/src/components/IIIFThumbnail.js
+++ b/src/components/IIIFThumbnail.js
@@ -44,6 +44,18 @@ export class IIIFThumbnail extends Component {
     }
   }
 
+  /**
+   * Handles the intersection (visibility) of a given thumbnail, by requesting
+   * the image and then updating the state.
+   */
+  handleIntersection(event) {
+    const { loaded } = this.state;
+
+    if (loaded || !event.isIntersecting) return;
+
+    this.setState(state => ({ ...state, loaded: true }));
+  }
+
   /**
    *
   */
@@ -94,18 +106,6 @@ export class IIIFThumbnail extends Component {
     };
   }
 
-  /**
-   * Handles the intersection (visibility) of a given thumbnail, by requesting
-   * the image and then updating the state.
-   */
-  handleIntersection(event) {
-    const { loaded } = this.state;
-
-    if (loaded || !event.isIntersecting) return;
-
-    this.setState(state => ({ ...state, loaded: true }));
-  }
-
   /** */
   image() {
     const {
diff --git a/src/components/ManifestForm.js b/src/components/ManifestForm.js
index 8c35fc20c431e8dd4aa2b9c34f19469bd3c7af0e..c4e03ec51ec756c26c5ada8b82489bdbdf63d626 100644
--- a/src/components/ManifestForm.js
+++ b/src/components/ManifestForm.js
@@ -36,20 +36,6 @@ export class ManifestForm extends Component {
     }
   }
 
-  /**
-   * formSubmit - triggers manifest update and sets lastRequested
-   * @param  {Event} event
-   * @private
-   */
-  formSubmit(event) {
-    const { addResource, onSubmit } = this.props;
-    const { formValue } = this.state;
-    event.preventDefault();
-    onSubmit();
-    addResource(formValue);
-    this.setState({ formValue: '' });
-  }
-
   /**
    * Reset the form state
    */
@@ -73,6 +59,20 @@ export class ManifestForm extends Component {
     });
   }
 
+  /**
+   * formSubmit - triggers manifest update and sets lastRequested
+   * @param  {Event} event
+   * @private
+   */
+  formSubmit(event) {
+    const { addResource, onSubmit } = this.props;
+    const { formValue } = this.state;
+    event.preventDefault();
+    onSubmit();
+    addResource(formValue);
+    this.setState({ formValue: '' });
+  }
+
   /**
    * render
    * @return {String} - HTML markup for the component
diff --git a/src/components/SearchHit.js b/src/components/SearchHit.js
index f83fa2aa17fa7f706ff4f9842c8c01c7cb6787a8..f27228fe15fbc9fe02ad9cbaf0fcdb2ffab817f6 100644
--- a/src/components/SearchHit.js
+++ b/src/components/SearchHit.js
@@ -39,6 +39,15 @@ export class SearchHit extends Component {
     }
   }
 
+  /** */
+  handleClick() {
+    const {
+      annotation, annotationId, selectAnnotation,
+    } = this.props;
+
+    if (annotation && annotationId) selectAnnotation(annotationId);
+  }
+
   /**
    * Pass content describing the hit to the announcer prop (intended for screen readers)
    */
@@ -59,15 +68,6 @@ export class SearchHit extends Component {
     ].join(' '));
   }
 
-  /** */
-  handleClick() {
-    const {
-      annotation, annotationId, selectAnnotation,
-    } = this.props;
-
-    if (annotation && annotationId) selectAnnotation(annotationId);
-  }
-
   /** */
   render() {
     const {
diff --git a/src/components/SearchPanelControls.js b/src/components/SearchPanelControls.js
index 9a37eef51e310c506a6b050b01acda7fcba99e04..91d045ddedb98b7d208c802280ac7d4bbc9bc6e7 100644
--- a/src/components/SearchPanelControls.js
+++ b/src/components/SearchPanelControls.js
@@ -39,18 +39,6 @@ export class SearchPanelControls extends Component {
     }
   }
 
-  /** */
-  getSuggestions(value, { showEmpty = false } = {}) {
-    const { suggestions } = this.state;
-
-    const inputValue = deburr(value.trim()).toLowerCase();
-    const inputLength = inputValue.length;
-
-    return inputLength === 0 && !showEmpty
-      ? []
-      : suggestions;
-  }
-
   /** */
   handleChange(event, value, reason) {
     if (value) {
@@ -63,6 +51,18 @@ export class SearchPanelControls extends Component {
     }
   }
 
+  /** */
+  getSuggestions(value, { showEmpty = false } = {}) {
+    const { suggestions } = this.state;
+
+    const inputValue = deburr(value.trim()).toLowerCase();
+    const inputLength = inputValue.length;
+
+    return inputLength === 0 && !showEmpty
+      ? []
+      : suggestions;
+  }
+
   /** */
   fetchAutocomplete(value) {
     const { autocompleteService } = this.props;
diff --git a/src/components/SidebarIndexTableOfContents.js b/src/components/SidebarIndexTableOfContents.js
index fd596f7bdf6db194db781b58d387a70565d6deed..bea3b9c9d7841984402499ace1c848e58ae0cdf0 100644
--- a/src/components/SidebarIndexTableOfContents.js
+++ b/src/components/SidebarIndexTableOfContents.js
@@ -26,6 +26,20 @@ function getStartCanvasId(node) {
 
 /** */
 export class SidebarIndexTableOfContents extends Component {
+  /** */
+  handleKeyPressed(event, node) {
+    const { expandedNodeIds, toggleNode } = this.props;
+    if (event.key === 'Enter'
+      || event.key === ' '
+      || event.key === 'Spacebar') {
+      this.selectTreeItem(node);
+    }
+    if ((event.key === 'ArrowLeft' && expandedNodeIds.indexOf(node.id) !== -1)
+      || (event.key === 'ArrowRight' && expandedNodeIds.indexOf(node.id) === -1 && node.nodes.length > 0)) {
+      toggleNode(node.id);
+    }
+  }
+
   /** */
   selectTreeItem(node) {
     const { setCanvas, toggleNode, windowId } = this.props;
@@ -44,20 +58,6 @@ export class SidebarIndexTableOfContents extends Component {
     setCanvas(windowId, canvasId);
   }
 
-  /** */
-  handleKeyPressed(event, node) {
-    const { expandedNodeIds, toggleNode } = this.props;
-    if (event.key === 'Enter'
-      || event.key === ' '
-      || event.key === 'Spacebar') {
-      this.selectTreeItem(node);
-    }
-    if ((event.key === 'ArrowLeft' && expandedNodeIds.indexOf(node.id) !== -1)
-      || (event.key === 'ArrowRight' && expandedNodeIds.indexOf(node.id) === -1 && node.nodes.length > 0)) {
-      toggleNode(node.id);
-    }
-  }
-
   /** */
   buildTreeItems(nodes, visibleNodeIds, containerRef, nodeIdToScrollTo) {
     const { classes } = this.props;
diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js
index 252e458243868b6f5b6c6ece55639dd510548595..d887f8bf94143389fd6566b4ac6d7a5c7dd123b6 100644
--- a/src/components/ThumbnailNavigation.js
+++ b/src/components/ThumbnailNavigation.js
@@ -42,6 +42,27 @@ export class ThumbnailNavigation extends Component {
     }
   }
 
+  /** */
+  handleKeyUp(e) {
+    const { position } = this.props;
+    let nextKey = 'ArrowRight';
+    let previousKey = 'ArrowLeft';
+    if (position === 'far-right') {
+      nextKey = 'ArrowDown';
+      previousKey = 'ArrowUp';
+    }
+    switch (e.key) {
+      case nextKey:
+        this.nextCanvas();
+        break;
+      case previousKey:
+        this.previousCanvas();
+        break;
+      default:
+        break;
+    }
+  }
+
   /**
    * When on right, row height
    * When on bottom, column width
@@ -128,27 +149,6 @@ export class ThumbnailNavigation extends Component {
     return canvasGroupings.length;
   }
 
-  /** */
-  handleKeyUp(e) {
-    const { position } = this.props;
-    let nextKey = 'ArrowRight';
-    let previousKey = 'ArrowLeft';
-    if (position === 'far-right') {
-      nextKey = 'ArrowDown';
-      previousKey = 'ArrowUp';
-    }
-    switch (e.key) {
-      case nextKey:
-        this.nextCanvas();
-        break;
-      case previousKey:
-        this.previousCanvas();
-        break;
-      default:
-        break;
-    }
-  }
-
   /**
    */
   nextCanvas() {
diff --git a/src/components/WorkspaceAdd.js b/src/components/WorkspaceAdd.js
index c0b91f38f20447cb56704ce43cd9efdb721f2d8e..696038d3b07fec4144f26e03cce0f38bc8333cb3 100644
--- a/src/components/WorkspaceAdd.js
+++ b/src/components/WorkspaceAdd.js
@@ -36,19 +36,6 @@ export class WorkspaceAdd extends React.Component {
     this.handleDrop = this.handleDrop.bind(this);
   }
 
-  /** @private */
-  onSubmit() {
-    this.setAddResourcesVisibility(false);
-    this.scrollToTop();
-  }
-
-  /**
-   * @private
-   */
-  setAddResourcesVisibility(bool) {
-    this.setState({ addResourcesOpen: bool });
-  }
-
   /** */
   handleDrop({ manifestId, manifestJson }, props, monitor) {
     const { addResource } = this.props;
@@ -62,6 +49,19 @@ export class WorkspaceAdd extends React.Component {
     this.scrollToTop();
   }
 
+  /** @private */
+  onSubmit() {
+    this.setAddResourcesVisibility(false);
+    this.scrollToTop();
+  }
+
+  /**
+   * @private
+   */
+  setAddResourcesVisibility(bool) {
+    this.setState({ addResourcesOpen: bool });
+  }
+
   /** Scroll the list back to the top */
   scrollToTop() {
     if (this.ref.current) {
diff --git a/src/components/WorkspaceExport.js b/src/components/WorkspaceExport.js
index c752f393cbde46c1d88cfb9dab2c87a80ddcecf5..a9ce6ed290f8274d88a1912ad26e115a3b25d0bc 100644
--- a/src/components/WorkspaceExport.js
+++ b/src/components/WorkspaceExport.js
@@ -23,11 +23,6 @@ export class WorkspaceExport extends Component {
     this.handleClose = this.handleClose.bind(this);
   }
 
-  /** Show the snackbar */
-  onCopy() {
-    this.setState({ copied: true });
-  }
-
   /** Handle closing after the content is copied and the snackbar is done */
   handleClose() {
     const { handleClose } = this.props;
@@ -35,6 +30,11 @@ export class WorkspaceExport extends Component {
     handleClose();
   }
 
+  /** Show the snackbar */
+  onCopy() {
+    this.setState({ copied: true });
+  }
+
   /**
    * @private
    */