diff --git a/.travis.yml b/.travis.yml
index 26694aaaf580e3e50c5b4e1225f26dd5c932d8bd..74c62d2296d0fdc7dfc709fd5387f487a81c1491 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,6 +35,19 @@ install:
   # Install project dependencies
   - pip install -r requirements.txt
 
+  # Install test dependencies
+  - pip install flake8
+
+  # Install node
+  - nvm install 8
+
+  # Install node dependencies
+  - cd ./opentech/static_src && npm install --quiet
+
+  # Build the static files
+  - cd ./opentech/static_src && npm run build:prod
+
+
 # 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/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/people/static/people/admin/js/update_person_title.js b/opentech/people/static/people/admin/js/update_person_title.js
new file mode 100644
index 0000000000000000000000000000000000000000..589f89eec9ca9264439f3404a7bb5ca4e3fe246e
--- /dev/null
+++ b/opentech/people/static/people/admin/js/update_person_title.js
@@ -0,0 +1,21 @@
+$(document).ready(function () {
+    var $lastNameInput = $('#id_last_name');
+    var $firstNameInput = $('#id_first_name');
+    var $titleInput = $('#id_title');
+    var $slugInput = $('#id_slug');
+
+    $firstNameInput.on('input', function () {joinFirstNameLastName();});
+
+    $lastNameInput.on('input', function () {joinFirstNameLastName();});
+
+    function joinFirstNameLastName() {
+        var firstName = $firstNameInput.val();
+        var lastName = $lastNameInput.val();
+        var title = firstName + ' ' + lastName;
+
+        $slugInput.data('previous-val', $slugInput.val());
+        $titleInput.data('previous-val', $titleInput.val());
+        $titleInput.val(title);
+        $titleInput.blur();  // Trigger slug update
+    }
+});
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_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/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/vagrant/provision.sh b/vagrant/provision.sh
index 77866f4b201720c13e624d15e2813e5d6635835b..4ccbdd8e3e8074fb114489a7c516abf17d00a482 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/$DEST_DIR/$PROJECT_NAME/$PROJECT_NAME/static_src; yarn install"
+su - vagrant -c "cd $PROJECT_DIR/$DEST_DIR/$PROJECT_NAME/$PROJECT_NAME/static_src; yarn build"