diff --git a/.travis.yml b/.travis.yml
index 26694aaaf580e3e50c5b4e1225f26dd5c932d8bd..150e48978b6f17689d62fbe7b685bc977ed0bf4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,9 +32,25 @@ before_script:
 # Package installation
 install:
   - pip install codecov
+
   # Install project dependencies
   - pip install -r requirements.txt
 
+  # Install node
+  - nvm install 8
+
+  # Move into the static_src folder where we will compile the FE
+  - cd ./opentech/static_src
+
+  # Install node dependencies
+  - npm install --quiet
+
+  # Build the static files
+  - npm run build:prod
+
+  # Change back to the original folder
+  - cd -
+
 # Run the tests
 script:
   # Run python code style checks
diff --git a/README.md b/README.md
index c7d2f35c63dc2c8bb3362e9944f79d43d558efc0..df325faa33aca6784af37b181e23253a98f74981 100644
--- a/README.md
+++ b/README.md
@@ -34,3 +34,19 @@ djrun
 ```
 
 This will make the site available on the host machine at: http://127.0.0.1:8000/
+
+# Updating front-end files
+
+Any changes made to sass or js files will need to be recompiled using:
+
+``` bash
+yarn build
+```
+
+Alternatively you can run the watcher that will rebuild on change to files:
+
+``` bash
+yarn start
+```
+
+Both commands should be run from within the `opentech/static_src` folder in the vagrant box.
diff --git a/opentech/categories/__init__.py b/opentech/categories/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/opentech/categories/migrations/0001_initial.py b/opentech/categories/migrations/0001_initial.py
deleted file mode 100644
index d63f218ef6f3c5ba955c7cf034e2451da4bda1dd..0000000000000000000000000000000000000000
--- a/opentech/categories/migrations/0001_initial.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10 on 2016-10-31 10:47
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='Category',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128)),
-            ],
-            options={
-                'verbose_name_plural': 'categories',
-                'verbose_name': 'category',
-            },
-        ),
-    ]
diff --git a/opentech/categories/migrations/__init__.py b/opentech/categories/migrations/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/opentech/categories/models.py b/opentech/categories/models.py
deleted file mode 100644
index ec548ebc898650d14a559217053162a69ef3b30e..0000000000000000000000000000000000000000
--- a/opentech/categories/models.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from django.db import models
-
-from wagtail.wagtailsnippets.models import register_snippet
-from wagtail.wagtailadmin.edit_handlers import FieldPanel
-
-
-@register_snippet
-class Category(models.Model):
-    name = models.CharField(max_length=128)
-
-    panels = [
-        FieldPanel('name', classname="full"),
-    ]
-
-    class Meta:
-        verbose_name = 'category'
-        verbose_name_plural = 'categories'
-
-    def __str__(self):
-        return self.name
diff --git a/opentech/navigation/migrations/0001_initial.py b/opentech/navigation/migrations/0001_initial.py
index 260fefa7bbcf3ab1d839a55b466712ad9adc3c3a..f213cf80464613397892157f8b299f6320a4fbd8 100644
--- a/opentech/navigation/migrations/0001_initial.py
+++ b/opentech/navigation/migrations/0001_initial.py
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('primary_navigation', wagtail.wagtailcore.fields.StreamField((('link', wagtail.wagtailcore.blocks.StructBlock((('page', wagtail.wagtailcore.blocks.PageChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(help_text="Leave blank to use the page's own title", required=False))))),), blank=True, help_text='Main site navigation')),
                 ('secondary_navigation', wagtail.wagtailcore.fields.StreamField((('link', wagtail.wagtailcore.blocks.StructBlock((('page', wagtail.wagtailcore.blocks.PageChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(help_text="Leave blank to use the page's own title", required=False))))),), blank=True, help_text='Alternative navigation')),
-                ('footer_navigation', wagtail.wagtailcore.fields.StreamField((('column', wagtail.wagtailcore.blocks.StructBlock((('heading', wagtail.wagtailcore.blocks.CharBlock(blank=True, help_text='Leave blank if no header required.')), ('links', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailcore.blocks.StructBlock((('page', wagtail.wagtailcore.blocks.PageChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(help_text="Leave blank to use the page's own title", required=False))))))))),), blank=True, help_text='Multiple columns of footer links with optional header.')),
+                ('footer_navigation', wagtail.wagtailcore.fields.StreamField((('column', wagtail.wagtailcore.blocks.StructBlock((('heading', wagtail.wagtailcore.blocks.CharBlock(required=False, help_text='Leave blank if no header required.')), ('links', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailcore.blocks.StructBlock((('page', wagtail.wagtailcore.blocks.PageChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(help_text="Leave blank to use the page's own title", required=False))))))))),), blank=True, help_text='Multiple columns of footer links with optional header.')),
                 ('footer_links', wagtail.wagtailcore.fields.StreamField((('link', wagtail.wagtailcore.blocks.StructBlock((('page', wagtail.wagtailcore.blocks.PageChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(help_text="Leave blank to use the page's own title", required=False))))),), blank=True, help_text='Single list of elements at the base of the page.')),
                 ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')),
             ],
diff --git a/opentech/navigation/models.py b/opentech/navigation/models.py
index 53d73d84a81d4077167b85fea9a9b15fa93b2202..c270b1478b29b4a670b1e6a07e2c667da83a485d 100644
--- a/opentech/navigation/models.py
+++ b/opentech/navigation/models.py
@@ -16,7 +16,7 @@ class LinkBlock(blocks.StructBlock):
 
 
 class LinkColumnWithHeader(blocks.StructBlock):
-    heading = blocks.CharBlock(blank=True, help_text="Leave blank if no header required.")
+    heading = blocks.CharBlock(required=False, help_text="Leave blank if no header required.")
     links = blocks.ListBlock(LinkBlock())
 
     class Meta:
diff --git a/opentech/news/migrations/0001_initial.py b/opentech/news/migrations/0001_initial.py
index e8a803d18e3967ea92ed538b976e8600fdde2ea3..e9c99a7d7fc3d694c5694a61876a858673f50f38 100644
--- a/opentech/news/migrations/0001_initial.py
+++ b/opentech/news/migrations/0001_initial.py
@@ -55,7 +55,7 @@ class Migration(migrations.Migration):
                 ('listing_summary', models.CharField(blank=True, help_text="The text summary used when this page appears in listings. It's also used as the description for search engines if the 'Search description' field above is not defined.", max_length=255)),
                 ('publication_date', models.DateTimeField(blank=True, help_text='Use this field to override the date that the news item appears to have been published.', null=True)),
                 ('introduction', models.TextField(blank=True)),
-                ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('citation_link', wagtail.wagtailcore.blocks.URLBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))))),
+                ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('attribution', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))))),
                 ('listing_image', models.ForeignKey(blank=True, help_text='Choose the image you wish to be displayed when this page appears in listings', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')),
                 ('social_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')),
             ],
diff --git a/opentech/people/migrations/0001_initial.py b/opentech/people/migrations/0001_initial.py
index cc6045ca54918ef5ad377f2895db81765c085450..61ae8c60b60c76bdf7652dba8741890398c2cde7 100644
--- a/opentech/people/migrations/0001_initial.py
+++ b/opentech/people/migrations/0001_initial.py
@@ -55,7 +55,7 @@ class Migration(migrations.Migration):
                 ('job_title', models.CharField(max_length=255)),
                 ('introduction', models.TextField(blank=True)),
                 ('website', models.URLField(blank=True, max_length=255)),
-                ('biography', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('citation_link', wagtail.wagtailcore.blocks.URLBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))), blank=True)),
+                ('biography', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('attribution', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))), blank=True)),
                 ('email', models.EmailField(blank=True, max_length=254)),
                 ('mobile_phone', models.CharField(blank=True, max_length=255)),
                 ('landline_phone', models.CharField(blank=True, max_length=255)),
diff --git a/opentech/static/js/update_person_title.js b/opentech/people/static/people/admin/js/update_person_title.js
similarity index 100%
rename from opentech/static/js/update_person_title.js
rename to opentech/people/static/people/admin/js/update_person_title.js
diff --git a/opentech/people/wagtail_hooks.py b/opentech/people/wagtail_hooks.py
index 9100b81a87ef1972677a473416ebb5e214875d8c..ceaff4b99a6bf31b8d162fda52427f98ac8b0a90 100644
--- a/opentech/people/wagtail_hooks.py
+++ b/opentech/people/wagtail_hooks.py
@@ -7,5 +7,5 @@ from wagtail.wagtailcore import hooks
 @hooks.register('insert_editor_js')
 def editor_js():
     return mark_safe(
-        '<script src="%s"></script>' % static('js/update_person_title.js')
+        '<script src="%s"></script>' % static('people/admin/js/update_person_title.js')
     )
diff --git a/opentech/standardpages/migrations/0001_initial.py b/opentech/standardpages/migrations/0001_initial.py
index 51278eef35460d11c6b12f0eab23dc09dc91a7bb..3e244f23e4db22de784f3a412a9da5071b082214 100644
--- a/opentech/standardpages/migrations/0001_initial.py
+++ b/opentech/standardpages/migrations/0001_initial.py
@@ -48,7 +48,7 @@ class Migration(migrations.Migration):
                 ('listing_title', models.CharField(blank=True, help_text='Override the page title used when this page appears in listings', max_length=255)),
                 ('listing_summary', models.CharField(blank=True, help_text="The text summary used when this page appears in listings. It's also used as the description for search engines if the 'Search description' field above is not defined.", max_length=255)),
                 ('introduction', models.TextField(blank=True)),
-                ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('citation_link', wagtail.wagtailcore.blocks.URLBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))))),
+                ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title', icon='title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('quote', wagtail.wagtailcore.blocks.StructBlock((('quote', wagtail.wagtailcore.blocks.CharBlock(classname='title')), ('attribution', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('embed', wagtail.wagtailembeds.blocks.EmbedBlock()), ('call_to_action', wagtail.wagtailsnippets.blocks.SnippetChooserBlock('utils.CallToActionSnippet', template='blocks/call_to_action_block.html')), ('document', wagtail.wagtailcore.blocks.StructBlock((('document', wagtail.wagtaildocs.blocks.DocumentChooserBlock()), ('title', wagtail.wagtailcore.blocks.CharBlock(required=False)))))))),
                 ('listing_image', models.ForeignKey(blank=True, help_text='Choose the image you wish to be displayed when this page appears in listings', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')),
                 ('social_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')),
             ],
diff --git a/opentech/static/css/main.css b/opentech/static/css/main.css
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/opentech/static/js/main.js b/opentech/static/js/main.js
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/opentech/static_src/package.json b/opentech/static_src/package.json
index 44baaff128e2a57e3ed984c55b13427561676a22..0e844e9bf69b734f6d33e5ac9d595b33fa531fa7 100755
--- a/opentech/static_src/package.json
+++ b/opentech/static_src/package.json
@@ -69,7 +69,7 @@
     "watch": "npm-run-all -p watch:*",
 
     "//[ Syncs ]//": "",
-    "sync:fonts": "rsync -rtvu --delete $npm_package_config_src_font/ $npm_package_config_dest_font/",
+    "sync:font": "rsync -rtvu --delete $npm_package_config_src_font/ $npm_package_config_dest_font/",
     "sync:img": "rsync -rtvu --delete $npm_package_config_src_img/ $npm_package_config_dest_img/",
     "sync": "npm-run-all -p sync:*",
 
diff --git a/opentech/static_src/yarn.lock b/opentech/static_src/yarn.lock
index 0cd3e3b5c862dbbe036be008d4f498645defddaa..6e9ff911ec51884107780ffd3464ad4bc740b272 100755
--- a/opentech/static_src/yarn.lock
+++ b/opentech/static_src/yarn.lock
@@ -186,10 +186,6 @@ async@0.2.x, async@~0.2.6, async@~0.2.9:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7"
-
 async@1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -916,10 +912,6 @@ colors@0.6.x, colors@~0.6.x:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
 
-colors@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
-
 combined-stream@^1.0.5, combined-stream@~1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
@@ -1001,10 +993,6 @@ core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
 
-corser@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
-
 cross-spawn@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
@@ -1174,15 +1162,6 @@ ecc-jsbn@~0.1.1:
   dependencies:
     jsbn "~0.1.0"
 
-ecstatic@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-1.4.1.tgz#32cb7b6fa2e290d58668674d115e8f0c3d567d6a"
-  dependencies:
-    he "^0.5.0"
-    mime "^1.2.11"
-    minimist "^1.1.0"
-    url-join "^1.0.0"
-
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -1900,10 +1879,6 @@ hawk@~3.1.3:
     hoek "2.x.x"
     sntp "1.x.x"
 
-he@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2"
-
 hoek@2.x.x:
   version "2.16.3"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@@ -1927,26 +1902,13 @@ http-errors@~1.5.0:
     setprototypeof "1.0.2"
     statuses ">= 1.3.1 < 2"
 
-http-proxy@1.15.2, http-proxy@^1.8.1:
+http-proxy@1.15.2:
   version "1.15.2"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.15.2.tgz#642fdcaffe52d3448d2bda3b0079e9409064da31"
   dependencies:
     eventemitter3 "1.x.x"
     requires-port "1.x.x"
 
-http-server@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.9.0.tgz#8f1b06bdc733618d4dc42831c7ba1aff4e06001a"
-  dependencies:
-    colors "1.0.3"
-    corser "~2.0.0"
-    ecstatic "^1.4.0"
-    http-proxy "^1.8.1"
-    opener "~1.4.0"
-    optimist "0.6.x"
-    portfinder "0.4.x"
-    union "~0.4.3"
-
 http-signature@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
@@ -2486,7 +2448,7 @@ mime@1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.4.tgz#11b5fdaf29c2509255176b80ad520294f5de92b7"
 
-mime@1.3.4, "mime@>= 0.0.1", mime@^1.2.11:
+mime@1.3.4, "mime@>= 0.0.1":
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
 
@@ -2503,7 +2465,7 @@ minimatch@~0.2.11, minimatch@~0.2.x:
     lru-cache "2"
     sigmund "~1.0.0"
 
-minimist@0.0.8, minimist@~0.0.1:
+minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
@@ -2511,7 +2473,7 @@ minimist@1.1.x:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
 
-minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0:
+minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
@@ -2519,7 +2481,7 @@ mkdirp@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
 
-mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
+mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -2748,10 +2710,6 @@ onetime@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
 
-opener@~1.4.0:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
-
 openurl@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.0.tgz#e2f2189d999c04823201f083f0f1a7cd8903187a"
@@ -2763,13 +2721,6 @@ opn@4.0.2:
     object-assign "^4.0.1"
     pinkie-promise "^2.0.0"
 
-optimist@0.6.x:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
-  dependencies:
-    minimist "~0.0.1"
-    wordwrap "~0.0.2"
-
 optionator@^0.8.1, optionator@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
@@ -2903,13 +2854,6 @@ pluralize@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
 
-portfinder@0.4.x:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-0.4.0.tgz#a3ffadffafe4fb98e0601a85eda27c27ce84ca1e"
-  dependencies:
-    async "0.9.0"
-    mkdirp "0.5.x"
-
 portscanner@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.1.1.tgz#eabb409e4de24950f5a2a516d35ae769343fbb96"
@@ -3000,10 +2944,6 @@ qs@6.2.1:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
 
-qs@~2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404"
-
 randomatic@^1.1.3:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"
@@ -3835,20 +3775,10 @@ underscore@1.7.x, underscore@~1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209"
 
-union@~0.4.3:
-  version "0.4.6"
-  resolved "https://registry.yarnpkg.com/union/-/union-0.4.6.tgz#198fbdaeba254e788b0efcb630bc11f24a2959e0"
-  dependencies:
-    qs "~2.3.3"
-
 unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
-url-join@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78"
-
 user-home@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
@@ -3955,7 +3885,7 @@ winston@0.8.x:
     pkginfo "0.3.x"
     stack-trace "0.0.x"
 
-wordwrap@0.0.2, wordwrap@~0.0.2:
+wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
 
diff --git a/opentech/templates/blocks/quote_block.html b/opentech/templates/blocks/quote_block.html
index 5f95799aa2432c275768ce57f06367340b67aea5..33bea5ac66717619297c0c58edbf8ab7ba740b9f 100644
--- a/opentech/templates/blocks/quote_block.html
+++ b/opentech/templates/blocks/quote_block.html
@@ -1,3 +1,4 @@
-<blockquote{% if value.citation_link %} cite="{{ value.citation_link }}"{% endif %}>
-    {{ value.quote }}
+<blockquote>
+    <p>{{ value.quote }}</p>
+    {% if value.attribution %}{{ value.attribution }}{% endif %}
 </blockquote>
diff --git a/opentech/utils/blocks.py b/opentech/utils/blocks.py
index 7c97eec039f4668b3ae8ce32dbdaacd0300ea251..973b0372b3313c3e40a8fb200b7bdddcb27db742 100644
--- a/opentech/utils/blocks.py
+++ b/opentech/utils/blocks.py
@@ -25,7 +25,7 @@ class DocumentBlock(blocks.StructBlock):
 
 class QuoteBlock(blocks.StructBlock):
     quote = blocks.CharBlock(classname="title")
-    citation_link = blocks.URLBlock(required=False)
+    attribution = blocks.CharBlock(required=False)
 
     class Meta:
         icon = "openquote"
diff --git a/setup.cfg b/setup.cfg
index 4ab5cc3deff7bec1cd567a0cbb99204f57f8bd0b..78add7cc2b71d13c7ef14c2f8ae3e2cc1291f4ce 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,5 +13,5 @@ ignore_errors = False
 disallow_untyped_defs = True
 
 [flake8]
-ignore=E501,F405
-exclude=*/migrations/*
+ignore = E501,F405
+exclude = migrations,node_modules
diff --git a/vagrant/provision.sh b/vagrant/provision.sh
index 77866f4b201720c13e624d15e2813e5d6635835b..83db9cd321a1be84a54e52b779e59eac66038562 100755
--- a/vagrant/provision.sh
+++ b/vagrant/provision.sh
@@ -50,7 +50,13 @@ alias djrunp="dj runserver_plus 0.0.0.0:8000"
 source $VIRTUALENV_DIR/bin/activate
 export PS1="[$PROJECT_NAME \W]\\$ "
 cd $PROJECT_DIR
+EOF
 
-alias djtestapply="dj test opentech.apply --keepdb; mypy ."
+# Install node.js and npm
+su - vagrant -c "curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -"
+su - vagrant -c "sudo apt-get install -y nodejs"
 
-EOF
+# Build the static files
+su - vagrant -c "sudo npm install -g yarn"
+su - vagrant -c "cd $PROJECT_DIR/$MODULE_NAME/static_src; yarn install"
+su - vagrant -c "cd $PROJECT_DIR/$MODULE_NAME/static_src; yarn build"