Repository: zooniverse/Panoptes-Front-End
Pull request #4402 (2018-02-26 20:36:36)

Project External Links Block
Staging branch URL: https://project-external-links.pfe-preview.zooniverse.org

Related to #4371. We discussed trying to move all external links into its own div block similar to organization home pages. This is a early version of this design idea.

@beckyrother I'll need some design guidance how this looks on organization home pages and project home pages:
Snapshot Safari organization: https://project-external-links.pfe-preview.zooniverse.org/organizations/meredithspalmer/snapshot-safari?env=production
Snapshot Godwana (a project with a lot of external links): https://project-external-links.pfe-preview.zooniverse.org/projects/meredithspalmer/snapshot-gondwana?env=production

There are slight differences in the project and organization designs in terms of headers and font sizes. One way to resolve this would be to reuse this new ExternalLinks component between the project and organization and make sure the font sizes and headers match between the home pages.

Another issue is if the researcher quote and links area are both visible, then the layout doesn't really look good since the quote gets smashed a bit. This can be seen on the Snapshot Godwana. Reworking projects to be like organizations with its content layout isn't going to work 1:1 since project introductions are typically shorter and are in the project hero. Organizations have their introductions further down the page and paired with the quote from the researcher. Let me know how you'd like to resolve this.

One last thing, there was an inconsistency with the organization links. In the project hero, it was treated as an internal link as it should be, since it doesn't go off site. In the navbar it was treated as an external link that opened a new tab and had the external link icon. I changed the org link in the navbar behave like an internal link. 

# Required Manual Testing

- [ ] Does the non-logged in home page render correctly?
- [ ] Does the logged in home page render correctly?
- [ ] Does the projects page render correctly?
- [ ] Can you load project home pages?
- [ ] Can you load the classification page?
- [ ] Can you submit a classification?
- [ ] Does talk load correctly?
- [ ] Can you post a talk comment?

# Review Checklist

- [x] Does it work in all major browsers: Firefox, Chrome, Edge, Safari?
- [x] Does it work on mobile?
- [x] Can you `rm -rf node_modules/ && npm install` and app works as expected?
- [ ] If the component is in coffeescript, is it converted to ES6? Is it free of eslint errors? Is the conversion its own commit?
- [x] Are the tests passing locally and on Travis?

# Optional

- [ ] Have you replaced any `ChangeListener` or `PromiseRenderer` components with code that updates component state?
- [ ] If changes are made to the classifier, does the dev classifier still work?
- [ ] Have you [resized and compressed](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/image-optimization) any image you've added?
- [ ] Have you added in [flow type annotations](https://flowtype.org/docs/type-annotations.html)?
- [ ] Have you followed the [Springer guidelines for commit messages](https://github.com/springernature/frontend-playbook/blob/master/git/git.md#commit-messages)?


beckyrother: I'd like to make sure Organizations is visually different from the projects within it, so it would be good to keep the differences in the header – is that what you're asking? 

However, I do agree to reuse the ExternalLinks component so it looks the same across everything. This is a long quote on the Gondwana project and I'm not sure how to resolve that, unless the modules were evenly split 1:1:1 rather than proportionally as they are now. Maybe try that and see if it helps? 
srallen: @beckyrother this is ready for another design review. 

- What an organization with external links looks like: https://project-external-links.pfe-preview.zooniverse.org/organizations/markb-panoptes/nfnorgtest
- An organization without external links: https://project-external-links.pfe-preview.zooniverse.org/organizations/markb-panoptes/urbanwildlifeorgtest
- A project with only external links: https://project-external-links.pfe-preview.zooniverse.org/projects/rafe-dot-lafrance/notes-from-nature-on-staging
- A project with only a researcher quote: https://project-external-links.pfe-preview.zooniverse.org/projects/srallen086/workflow-assignment-testing
- A project with both: https://project-external-links.pfe-preview.zooniverse.org/projects/srallen086/srallen-testing
- A project with none: https://project-external-links.pfe-preview.zooniverse.org/projects/srallen086/galaxy-zoo-in-astronomy-101
srallen: Note to self: The project about section isn't wrapping properly on smaller screen sizes anymore.
srallen: This is ready for technical review. A few things to note in addition to the above design comments:
- I did a organizational refactor to put the `ExternalLinksBlock` into the top level components folder and put the helpers into the lib folder and the test helpers into the top level test folder. This is because the component is now shared between the organization page and project home page.
- I removed the redundancy between the `social-links.js` and `socialIcons.json` file and preferred the later. The social links editor in the lab had a tiny refactor because of it.
- I fixed some incorrect CSS with the researcher quote css. It was using both flex and block layout floats, which according to spec don't work together. It's been refactored to properly use flex. 
- Acknowledging here that IE 11 does some unexpected wrapping with the ExternalLinksBlock, but I don't think it's worth the effort in fixing since it doesn't really look bad, in my opinion. IE 11 has a lot of known flex bugs and we should encourage folks to use Edge instead of continuing to fix every IE 11 bug we see. 
rogerhutchings: I had a few observations on the design first (some of these may be nitpicking):

**Project external links block**

<img width="296" alt="screenshot 2018-03-06 11 27 11" src="https://user-images.githubusercontent.com/2725841/37030125-aaa0d890-2131-11e8-9bea-1f83e7e06565.png">

**Org external links block**

<img width="341" alt="screenshot 2018-03-06 11 25 58" src="https://user-images.githubusercontent.com/2725841/37030120-a7e844f8-2131-11e8-876a-5a89f2153b6a.png">

**Observations**

- Heading font size is very small on the org version.
- Font family for the block is Open Sans on the project, Karla on the org (although I appreciate that the project page still needs updating).
- This might be a personal thing, but the underline on hover under the icon looks a bit ugly, and isn't always visible depending on the icon and browser. The screenshots were taken on Chrome, and the underline isn't visible at all on Chrome or Safari. It _is_ visible on Firefox, but it's right up against the underside of the icon.
- Icons are right-aligned, so wider icons like G+ look a bit offset.
srallen: @rogerhutchings to answer the design questions:
- @beckyrother asked for visual differentiation in the headers between orgs and projects and confirmed in this comment: https://github.com/zooniverse/Panoptes-Front-End/pull/4402#issuecomment-368962189
- Font family should be updated, but I'd probably do that in a separate PR

For the last two points, we can check in with what @beckyrother would like. I added the underline because the org version was missing hover, focus states entirely, so it was a quick and easy way to make them more accessible. I don't personally have a preference as to what the hover, focus state is. Perhaps border-bottom instead so it's more visually prominent? 
beckyrother: - Extremely agree about the font – should definitely be switched to Karla and make sure the size is the same. 

- We (and most of the internet) treat every other text link with an underline, so for consistency's sake please keep it an underline. If you'd like to make it border-bottom so it's thicker and matches the nav bar, that's fine, but it needs to be an underline.

- Not a lot we can do about the G+ icon, that's the width that it is. I prefer the icons to be right-aligned so there's a consistent column of white space between icons and their labels.

rogerhutchings: Thanks @srallen & @beckyrother! I'm not questioning underlining links, just the icon as it stands. For what it's worth, I quickly mocked up a couple of alternatives in-browser:

`border-bottom`:

<img width="228" alt="screenshot 2018-03-06 21 22 12" src="https://user-images.githubusercontent.com/2725841/37059404-44d60d74-2185-11e8-835a-959399120d71.png">

Underline on the text only:

<img width="193" alt="screenshot 2018-03-06 21 26 34" src="https://user-images.githubusercontent.com/2725841/37059421-539a5752-2185-11e8-824a-fc68a4414046.png">

I'd prefer either to the current one, but this is a purely subjective thing and ultimately not my decision 😁🤐


beckyrother: @rogerhutchings Oh! I completely misunderstood, my mistake. You're totally right, the underline on the icon looks silly. The second option you provide is great! Happy to move forward with that one. 
srallen: @rogerhutchings this is ready for another review
srallen: @rogerhutchings do you have a moment to do a final review on this? It's been rebased with the latest master. Thanks!
rogerhutchings: LGTM 👍 
beckyrother: Should this have a grey background? Looking at Planet Four: Terrains, it's really awkward below the "1 person is talking about this" https://project-external-links.pfe-preview.zooniverse.org/projects/mschwamb/planet-four-terrains

![image](https://user-images.githubusercontent.com/1392130/37350230-3d0130ee-26a6-11e8-9b6d-2c9a5d224173.png)

srallen: @beckyrother do you mean the external links block area to have the grey background?
beckyrother: @srallen I'm sorry to keep adding things. Is that screenshot representative of the layout project pages will usually have? if it is, yes, it should have the same grey background as behind the stats. If not white is fine.
srallen: @beckyrother The launched approved projects will likely have commenters on Talk, so I'll make the background grey. Do you want it grey or white for organizations?
beckyrother: @srallen Sorry, for organizations it should stay white. 
srallen: @beckyrother I've updated the background colors, however, there seems to be even more variability here. The talk stat sometimes is next to talk subject images above the project stats instead of next to the project stats. Planet Four: Terrains loaded with the talk stats next to the project stats, because here how it looks for me with the grey background:

<img width="1437" alt="screen shot 2018-03-15 at 9 38 28 am" src="https://user-images.githubusercontent.com/5016731/37470066-abf757be-2834-11e8-9e6c-9edd3e1935a8.png">

I think the variability depends on the type of Talk comment, if it's for a subject or not maybe? Do you want to continue with this change? It looks ok in the other cases:

Staging organization with white background: http://localhost:3735/organizations/markb-panoptes/nfnorgtest
Staging project with grey background and the talk stat next to the project stats:
http://localhost:3735/projects/srallen086/srallen-testing

beckyrother: Nothing is ever simple! Let's leave it grey like this, I like the white-teal-grey in the image you posted. Approved on my end! Thank you Sarah :)
srallen: @beckyrother Ok! Should this merge or would you like to send the staged branch link out to the test listserv?
beckyrother: @srallen we should send this to the listserve again. I added a few more people to the spreadsheet and I'm so sorry but I'm not able to get into the mailing list to edit it. I will write a little thing and link to both my spreadsheet and the email so they can go out all at once though.
srallen: @beckyrother ok sounds good. Let's keep the spreadsheet link off of github just for privacy since it contains email addresses.
beckyrother: @srallen here's the email: https://docs.google.com/document/d/1O89hlcOktKhMhcWM-lR4vBr_lwejU-fPE6ud5WMzpMs/edit?usp=sharing
and i'll dm you the updated emails 
simoneduca: Is this waiting on some more feedback from the email that was sent?
beckyrother: Yes, I'm planning to leave the survey open till Friday, so we can merge Monday morning. 

Feedback:
Would it be possible to allow a project owner to specify that a single link stay in the nav bar? Meg says her field guide information is in an external link, and I'm not sure how many other projects do that but I don't want that information to be lost. Interested in hearing thoughts about this idea.
srallen: @beckyrother that's an edge case and an unlikely one for most projects. Planet Four: Terrains launched before we implemented field guides in the project builder, so this project is linking back to a page on the original Planet Four. The original Planet Four is also in the process of being migrated to Panoptes, so regardless, she'll need to migrate the Planet Four: Terrains field guide to the project builder version once the old Planet Four shuts down. I don't think we should come up with a solution just for this scenario of an external link and advise migrating it to the project builder instead. 
beckyrother: @srallen I completely agree about her information, she needs to migrate it to our Field Guide. 

We've only gotten 4 responses so far but there haven't been any other actionable requests. I'll post here again if that changes.
beckyrother: @srallen got one good comment from the survey: This section should be called "External Project Links" rather than just "Links" 
That's all! Survey closes at 5 today but I think we're ready to go after that minor text update.

Patch:
diff --git a/app/components/ExternalLinksBlock/ExternalLinksBlock.jsx b/app/components/ExternalLinksBlock/ExternalLinksBlock.jsx
new file mode 100644
index 00000000..6b55ad93
--- /dev/null
+++ b/app/components/ExternalLinksBlock/ExternalLinksBlock.jsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled, { ThemeProvider } from 'styled-components';
+import theme from 'styled-theming';
+import ExternalLink from './components/ExternalLink';
+import { zooTheme } from '../../theme';
+
+export const StyledExternalLinksBlock = styled.div`
+  background-color: ${({ isItAProject }) => {
+    return (isItAProject) ? zooTheme.colors.background : 'white';
+  }};
+  box-sizing: border-box;
+  color: ${theme('mode', {
+    light: zooTheme.colors.navy.default
+  })};
+  flex: ${props => `1 0 ${props.basis}%`};
+  min-width: 400px;
+  padding: 3em 4vw;
+
+  ul {
+    padding-left: 0;
+  }
+
+    li {
+      display: block;
+      margin-top: 1.5em;
+    }
+
+      a i {
+        margin: 0 1ch;
+        text-decoration: none;
+      }
+
+        &:hover, &:focus {
+          text-decoration: none;
+        }
+
+      .link-title:hover, .link-title:focus {
+          text-decoration: underline;
+        }
+
+`;
+
+export const StyledExternalLink = styled(ExternalLink)`
+  align-items: center;
+  display: inline-flex;
+  flex-direction: row-reverse;
+  font-size: 1.2em;
+  justify-content: flex-start;
+  text-decoration: none;
+`;
+
+function isResourceAProject(resource) {
+  return Object.keys(resource).includes('workflow_description');
+}
+
+export default function ExternalLinksBlock({ basis, header, links, resource }) {
+  return (
+    <ThemeProvider theme={{ mode: 'light' }}>
+      <StyledExternalLinksBlock basis={basis} isItAProject={isResourceAProject(resource)}>
+        {header}
+        <ul>
+          {links.map((link) => {
+            const { isExternalLink, isSocialLink, label, path, site, url } = link;
+            return (
+              <li key={url}>
+                <StyledExternalLink
+                  isExternalLink={isExternalLink}
+                  isSocialLink={isSocialLink}
+                  label={label}
+                  path={path}
+                  site={site}
+                  url={url}
+                />
+              </li>
+            );
+          })}
+        </ul>
+      </StyledExternalLinksBlock>
+    </ThemeProvider>
+  );
+}
+
+ExternalLinksBlock.defaultProps = {
+  basis: '33.333',
+  header: null,
+  links: [],
+  resource: null
+};
+
+ExternalLinksBlock.propTypes = {
+  basis: PropTypes.string,
+  header: PropTypes.node,
+  links: PropTypes.arrayOf(PropTypes.object),
+  resource: PropTypes.object
+};
+
diff --git a/app/components/ExternalLinksBlock/ExternalLinksBlock.spec.jsx b/app/components/ExternalLinksBlock/ExternalLinksBlock.spec.jsx
new file mode 100644
index 00000000..924d252c
--- /dev/null
+++ b/app/components/ExternalLinksBlock/ExternalLinksBlock.spec.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { expect } from 'chai';
+import Translate from 'react-translate-component';
+import ExternalLinksBlock, { StyledExternalLinksBlock, StyledExternalLink } from './ExternalLinksBlock';
+import { externalLinks } from '../../../test';
+
+describe('ExternalLinksBlock', function() {
+  let wrapper;
+  before(function() {
+    wrapper = shallow(
+      <ExternalLinksBlock
+        header={<Translate content="project.home.links" />}
+        links={externalLinks}
+        resource={{ workflow_description: '' }}
+      />
+    );
+  });
+
+  it('should render without crashing', function() {});
+
+  it('should use a ThemeProvider', function() {
+    expect(wrapper.find('ThemeProvider')).to.have.lengthOf(1);
+  });
+
+  it('should render a StyledExternalLinksBlock component', function() {
+    expect(wrapper.find(StyledExternalLinksBlock)).to.have.lengthOf(1);
+  });
+
+  it('should render StyledExternalLink components equal to the length of the links prop', function() {
+    expect(wrapper.find(StyledExternalLink)).to.have.lengthOf(externalLinks.length);
+  });
+
+  it('should render a Translate component for the header', function() {
+    expect(wrapper.find('Translate')).to.have.lengthOf(1);
+  });
+});
diff --git a/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.jsx b/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.jsx
new file mode 100644
index 00000000..303962af
--- /dev/null
+++ b/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import _ from 'lodash';
+import ExternalLinksBlock from './ExternalLinksBlock';
+
+export default class ExternalLinksBlockContainer extends React.Component {
+  getExternalLinks() {
+    const allExternalLinks = _.get(this.props.resource, 'urls', [])
+      .map(link => ({ ...link, isExternalLink: true }));
+
+    const partitionedLinks = _.partition(allExternalLinks, link => (link.site));
+
+    const external = partitionedLinks[1].map(link => ({
+      isExternalLink: true,
+      label: link.label,
+      url: link.url
+    }));
+
+    const social = partitionedLinks[0].map(link => ({
+      isExternalLink: true,
+      isSocialLink: true,
+      label: link.label,
+      path: link.path,
+      site: link.site,
+      url: link.url
+    }));
+
+    return {
+      external,
+      social
+    };
+  }
+
+  render() {
+    const { external, social } = this.getExternalLinks();
+    const links = [].concat(external, social);
+
+    if (links.length > 0) {
+      return (
+        <ExternalLinksBlock header={this.props.header} links={links} basis={this.props.basis} resource={this.props.resource} />
+      );
+    }
+
+    return null;
+  }
+}
diff --git a/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.spec.jsx b/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.spec.jsx
new file mode 100644
index 00000000..539b454e
--- /dev/null
+++ b/app/components/ExternalLinksBlock/ExternalLinksBlockContainer.spec.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import ExternalLinksBlockContainer from './ExternalLinksBlockContainer';
+import { projectWithoutRedirect } from '../../../test';
+
+describe('ExternalLinksBlockContainer', function() {
+  let wrapper;
+  let getExternalLinksSpy;
+  before(function() {
+    getExternalLinksSpy = sinon.spy(ExternalLinksBlockContainer.prototype, 'getExternalLinks');
+    getExternalLinksSpy.resetHistory();
+    wrapper = shallow(<ExternalLinksBlockContainer resource={{ id: '1' }} />);
+  });
+
+  after(function() {
+    getExternalLinksSpy.restore();
+  });
+
+  it('should call getExternalLinks on render', function () {
+    expect(getExternalLinksSpy.calledOnce).to.be.true;
+  });
+
+  it('should render null of the links length is 0', function() {
+    expect(wrapper.html()).to.be.null;
+  });
+
+  it('should render ExternalLinksBlock if the links length is greater than 0', function() {
+    wrapper.setProps({ resource: projectWithoutRedirect });
+    expect(wrapper.find('ExternalLinksBlock')).to.have.lengthOf(1);
+  });
+});
diff --git a/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.jsx b/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.jsx
new file mode 100644
index 00000000..f27926cd
--- /dev/null
+++ b/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { socialIcons } from '../../../../lib/nav-helpers';
+
+export default function ExternalLink({ className, isExternalLink, isSocialLink, label, path, site, url }) {
+  let iconClasses;
+  let linkLabel = label;
+  const linkProps = {
+    className,
+    href: url,
+    rel: 'noopener noreferrer',
+    target: '_blank'
+  };
+
+  if (isExternalLink) {
+    iconClasses = 'fa fa-external-link fa-fw';
+  }
+
+  if (isSocialLink) {
+    const icon = socialIcons[site].icon;
+    linkProps['aria-label'] = socialIcons[site].ariaLabel;
+    iconClasses = `fa ${icon} fa-fw`;
+    linkLabel = path;
+  }
+
+  if (isExternalLink || isSocialLink) {
+    return (
+      <a {...linkProps}>
+        <span className="link-title">{linkLabel}</span>
+        {iconClasses && <i className={iconClasses} />}
+      </a>
+    );
+  }
+
+  return null;
+}
+
+ExternalLink.defaultProps = {
+  className: '',
+  isExternalLink: false,
+  isSocialLink: false,
+  label: '',
+  path: '',
+  site: '',
+  url: ''
+};
+
+ExternalLink.propTypes = {
+  className: PropTypes.string,
+  isExternalLink: PropTypes.bool.isRequired,
+  isSocialLink: PropTypes.bool.isRequired,
+  label: PropTypes.string.isRequired,
+  path: PropTypes.string,
+  site: PropTypes.string,
+  url: PropTypes.string.isRequired
+};
diff --git a/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.spec.jsx b/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.spec.jsx
new file mode 100644
index 00000000..cb2c5ee8
--- /dev/null
+++ b/app/components/ExternalLinksBlock/components/ExternalLink/ExternalLink.spec.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { expect } from 'chai';
+import ExternalLink from './ExternalLink';
+import { socialIcons } from '../../../../lib/nav-helpers';
+
+const MOCK_EXTERNAL_URL = 'https://www.google.com';
+
+const MOCK_SOCIAL_URL = 'https://www.facebook.com/my-profile';
+
+const MOCK_SOCIAL_PATH = 'my-profile';
+
+const MOCK_SOCIAL_SITE = 'facebook.com/';
+
+describe('ExternalLink', function() {
+  let wrapper;
+  before(function() {
+    wrapper = shallow(<ExternalLink />);
+  });
+
+  it('should render without crashing', function() {});
+
+  it('should return null if isExternalLink or isSocialLink is false', function() {
+    expect(wrapper.html()).to.be.null;
+  });
+
+  describe('when the link is external', function() {
+    let wrapper;
+    before(function() {
+      wrapper = shallow(
+        <ExternalLink
+          isExternalLink={true}
+          url={MOCK_EXTERNAL_URL}
+        />
+      );
+    });
+
+    it('should add props to open the url in a new tab', function () {
+      expect(wrapper.props().target).to.equal('_blank');
+      expect(wrapper.props().rel).to.equal('noopener noreferrer');
+    });
+
+    it('should use props.url for the href', function () {
+      expect(wrapper.props().href).to.equal(MOCK_EXTERNAL_URL);
+    });
+
+    it('should have the Font Awesome external link icon', function () {
+      expect(wrapper.find('i').hasClass('fa-external-link')).to.be.true;
+    });
+  });
+
+  describe('when the link is to social media', function () {
+    let wrapper;
+    before(function() {
+      wrapper = shallow(
+        <ExternalLink
+          isExternalLink={true}
+          isSocialLink={true}
+          path={MOCK_SOCIAL_PATH}
+          site={MOCK_SOCIAL_SITE}
+          url={MOCK_SOCIAL_URL}
+        />);
+    });
+
+    it('should add props for the aria-label', function () {
+      expect(wrapper.props()['aria-label']).to.equal(socialIcons[MOCK_SOCIAL_SITE].ariaLabel);
+    });
+
+    it('should add props to open the url in a new tab', function () {
+      expect(wrapper.props().target).to.equal('_blank');
+      expect(wrapper.props().rel).to.equal('noopener noreferrer');
+    });
+
+    it('should use props.url for the href', function () {
+      expect(wrapper.props().href).to.equal(MOCK_SOCIAL_URL);
+    });
+
+    it('should use the correct font awesome icon', function () {
+      expect(wrapper.find('i').hasClass(socialIcons[MOCK_SOCIAL_SITE].icon)).to.be.true;
+    });
+
+    it('should use prop.path as the label', function () {
+      expect(wrapper.text().includes(MOCK_SOCIAL_PATH)).to.be.true;
+    });
+  });
+});
diff --git a/app/components/ExternalLinksBlock/components/ExternalLink/index.js b/app/components/ExternalLinksBlock/components/ExternalLink/index.js
new file mode 100644
index 00000000..c5456e29
--- /dev/null
+++ b/app/components/ExternalLinksBlock/components/ExternalLink/index.js
@@ -0,0 +1,1 @@
+export { default } from './ExternalLink';
\ No newline at end of file
diff --git a/app/components/ExternalLinksBlock/index.js b/app/components/ExternalLinksBlock/index.js
new file mode 100644
index 00000000..d4c3f821
--- /dev/null
+++ b/app/components/ExternalLinksBlock/index.js
@@ -0,0 +1,1 @@
+export { default } from './ExternalLinksBlockContainer';
\ No newline at end of file
diff --git a/app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.js b/app/lib/nav-helpers/getProjectLinks.js
similarity index 96%
rename from app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.js
rename to app/lib/nav-helpers/getProjectLinks.js
index 4e19b4a2..9c4a7330 100644
--- a/app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.js
+++ b/app/lib/nav-helpers/getProjectLinks.js
@@ -1,6 +1,6 @@
 import _ from 'lodash';
 
-import isAdmin from '../../../../../lib/is-admin';
+import isAdmin from '../is-admin';
 import userHasLabAccess from './userHasLabAccess';
 
 function getProjectLinks({ project, projectRoles, workflow, user }) {
diff --git a/app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.spec.js b/app/lib/nav-helpers/getProjectLinks.spec.js
similarity index 99%
rename from app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.spec.js
rename to app/lib/nav-helpers/getProjectLinks.spec.js
index 772027ae..6642f9b0 100644
--- a/app/pages/project/components/ProjectNavbar/helpers/getProjectLinks.spec.js
+++ b/app/lib/nav-helpers/getProjectLinks.spec.js
@@ -10,7 +10,7 @@ import {
   projectWithoutRedirect,
   randomUser,
   workflow
-} from '../testHelpers';
+} from '../../../test';
 
 
 describe('getProjectLinks', function() {
diff --git a/app/pages/project/components/ProjectNavbar/helpers/index.js b/app/lib/nav-helpers/index.js
similarity index 72%
rename from app/pages/project/components/ProjectNavbar/helpers/index.js
rename to app/lib/nav-helpers/index.js
index c23cabf7..6aee390e 100644
--- a/app/pages/project/components/ProjectNavbar/helpers/index.js
+++ b/app/lib/nav-helpers/index.js
@@ -1,7 +1,9 @@
 import getProjectLinks from './getProjectLinks';
+import socialIcons from './socialIcons';
 import userHasLabAccess from './userHasLabAccess';
 
 export {
   getProjectLinks,
+  socialIcons,
   userHasLabAccess
 };
diff --git a/app/lib/nav-helpers/socialIcons.json b/app/lib/nav-helpers/socialIcons.json
new file mode 100644
index 00000000..743e9c8c
--- /dev/null
+++ b/app/lib/nav-helpers/socialIcons.json
@@ -0,0 +1,67 @@
+{
+  "bitbucket.com/": {
+    "icon": "fa-bitbucket",
+    "ariaLabel": "Link to BitBucket repository",
+    "label": "BitBucket"
+  },
+  "facebook.com/": {
+    "icon": "fa-facebook-square",
+    "ariaLabel": "Link to Facebook Page",
+    "label": "Facebook"
+  },
+  "github.com/": {
+    "icon": "fa-github",
+    "ariaLabel": "Link to GitHub repository",
+    "label": "GitHub"
+  },
+  "pinterest.com/": {
+    "icon": "fa-pinterest",
+    "ariaLabel": "Link to Pinterest page",
+    "label": "Pinterest"
+  },
+  "plus.google.com/": {
+    "icon": "fa-google-plus",
+    "ariaLabel": "Link to Google+ page",
+    "label": "Google+"
+  },
+  "reddit.com/": {
+    "icon": "fa-reddit",
+    "ariaLabel": "Link to Reddit",
+    "label": "Reddit"
+  },
+  "tumblr.com/": {
+    "icon": "fa-tumblr",
+    "ariaLabel": "Link to Tumblr",
+    "label": "Tumblr"
+  },
+  "twitter.com/": {
+    "icon": "fa-twitter",
+    "ariaLabel": "Link to Twitter profile",
+    "label": "Twitter"
+  },
+  "weibo.com/": {
+    "icon": "fa-weibo",
+    "ariaLabel": "Link to Weibo page",
+    "label": "Weibo"
+  },
+  "wordpress.com/": {
+    "icon": "fa-wordpress",
+    "ariaLabel": "Link to WordPress site",
+    "label": "Wordpress"
+  },
+  "youtube.com/": {
+    "icon": "fa-youtube",
+    "ariaLabel": "Link to YouTube video",
+    "label": "YouTube"
+  },
+  "instagram.com/": {
+    "icon": "fa-instagram",
+    "ariaLabel": "Link to Instagram profile",
+    "label": "Instagram"
+  },
+  "medium.com/": {
+    "icon": "fa-medium",
+    "ariaLabel": "Link to Medium article",
+    "label": "Medium"
+  }
+}
diff --git a/app/pages/project/components/ProjectNavbar/helpers/userHasLabAccess.js b/app/lib/nav-helpers/userHasLabAccess.js
similarity index 100%
rename from app/pages/project/components/ProjectNavbar/helpers/userHasLabAccess.js
rename to app/lib/nav-helpers/userHasLabAccess.js
diff --git a/app/pages/project/components/ProjectNavbar/helpers/userHasLabAccess.spec.js b/app/lib/nav-helpers/userHasLabAccess.spec.js
similarity index 98%
rename from app/pages/project/components/ProjectNavbar/helpers/userHasLabAccess.spec.js
rename to app/lib/nav-helpers/userHasLabAccess.spec.js
index 18b9b03c..5bf74430 100644
--- a/app/pages/project/components/ProjectNavbar/helpers/userHasLabAccess.spec.js
+++ b/app/lib/nav-helpers/userHasLabAccess.spec.js
@@ -5,7 +5,7 @@ import {
   projectCollabUser,
   projectRoles,
   randomUser
-} from '../testHelpers';
+} from '../../../test';
 
 describe('userHasLabAccess', function() {
   it('returns false if projectRoles does not have any roles', function() {
diff --git a/app/locales/en.js b/app/locales/en.js
index b753fce9..9f0443d0 100644
--- a/app/locales/en.js
+++ b/app/locales/en.js
@@ -72,7 +72,8 @@ export default {
       learnMore: 'Learn more',
       getStarted: 'Get started',
       workflowAssignment: 'You\'ve unlocked level %(workflowDisplayName)s',
-      visitLink: 'Visit the project'
+      visitLink: 'Visit the project',
+      links: 'External Project Links'
     }
   },
   organization: {
diff --git a/app/locales/es.js b/app/locales/es.js
index cea7c512..1cd53aa8 100644
--- a/app/locales/es.js
+++ b/app/locales/es.js
@@ -70,7 +70,8 @@ export default {
       learnMore: 'Learn more',
       getStarted: 'Get started',
       workflowAssignment: 'You\'ve unlocked level %(workflowDisplayName)s',
-      visitLink: 'Visit the project'
+      visitLink: 'Visit the project',
+      links: 'Links'      
     }
   },
   organization: {
diff --git a/app/locales/it.js b/app/locales/it.js
index 58b922e0..f4be457e 100644
--- a/app/locales/it.js
+++ b/app/locales/it.js
@@ -70,7 +70,8 @@ export default {
       learnMore: 'Learn more',
       getStarted: 'Get started',
       workflowAssignment: 'You\'ve unlocked level %(workflowDisplayName)s',
-      visitLink: 'Visit the project'
+      visitLink: 'Visit the project',
+      links: 'Links'      
     }
   },
   tasks: {
diff --git a/app/locales/nl.js b/app/locales/nl.js
index da9309cf..7e656916 100644
--- a/app/locales/nl.js
+++ b/app/locales/nl.js
@@ -97,7 +97,8 @@ export default {
       learnMore: 'Learn more',
       getStarted: 'Get started',
       workflowAssignment: 'You\'ve unlocked level %(workflowDisplayName)s',
-      visitLink: 'Visit the project'
+      visitLink: 'Visit the project',
+      links: 'Links'
     }
   },
   organization: {
diff --git a/app/pages/lab/social-links-editor.jsx b/app/pages/lab/social-links-editor.jsx
index 5d43884a..f8a4beb0 100644
--- a/app/pages/lab/social-links-editor.jsx
+++ b/app/pages/lab/social-links-editor.jsx
@@ -2,14 +2,15 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import DragReorderable from 'drag-reorderable';
 import AutoSave from '../../components/auto-save.coffee';
-import SOCIAL_ICONS from '../../lib/social-icons';
+import { socialIcons } from '../../lib/nav-helpers';
 
 const ROUTE_BEFORE_DOMAIN = ['wordpress'];
 
 export default class SocialLinksEditor extends React.Component {
   constructor(props) {
     super(props);
-    const socialOrder = Object.keys(SOCIAL_ICONS).map(key => key);
+    const socialOrder = Object.keys(socialIcons).map(key => key);
+
     this.state = {
       socialOrder
     };
diff --git a/app/pages/organization/organization-page.jsx b/app/pages/organization/organization-page.jsx
index ca19071a..ea06ee8e 100644
--- a/app/pages/organization/organization-page.jsx
+++ b/app/pages/organization/organization-page.jsx
@@ -9,7 +9,7 @@ import Translate from 'react-translate-component';
 import OrganizationProjectCards from './organization-project-cards';
 import OrganizationMetadata from './organization-metadata';
 import Thumbnail from '../../components/thumbnail';
-import SocialIcons from '../../lib/social-icons';
+import ExternalLinksBlockContainer from '../../components/ExternalLinksBlock';
 
 const AVATAR_SIZE = 100;
 
@@ -194,31 +194,10 @@ class OrganizationPage extends React.Component {
                   </button>
                 </div>}
             </div>
-            <div className="organization-details__links">
-              <h4 className="organization-details__heading"><Translate content="organization.home.links" /></h4>
-              {(rearrangedLinks.length > 0) && rearrangedLinks.map((link, i) => {
-                let iconForLabel;
-                let label;
-                if (link.path) {
-                  iconForLabel = SocialIcons[link.site] || 'external-link';
-                  label = <span> - @{link.path}</span>;
-                } else {
-                  iconForLabel = 'external-link';
-                  label = <span> - {link.label}</span>;
-                }
-                return (
-                  <a
-                    key={link.key || link.path}
-                    className="organization-details__link"
-                    href={`${link.url}`}
-                    target={`${this.props.organization.id}${link.url}`}
-                    rel="noopener noreferrer"
-                  >
-                    <i className={`fa fa-${iconForLabel} fa-fw fa-2x organization-details__icon`} />
-                    {label}
-                  </a>);
-              })}
-            </div>
+            <ExternalLinksBlockContainer
+              header={<Translate className="organization-details__heading" content="organization.home.links" component="h4" />}
+              resource={this.props.organization}
+            />
           </div>
         </section>
       </div>
diff --git a/app/pages/organization/organization-page.spec.js b/app/pages/organization/organization-page.spec.js
index 4ef5c870..dca01b99 100644
--- a/app/pages/organization/organization-page.spec.js
+++ b/app/pages/organization/organization-page.spec.js
@@ -144,7 +144,6 @@ describe('OrganizationPage', function () {
 
   it('should show organization links', function () {
     const wrapper = shallow(<OrganizationPage organization={organization} />);
-    const links = wrapper.find('.organization-details__link');
-    assert.equal(links.length, organization.urls.length);
+    assert.equal(wrapper.find('ExternalLinksBlockContainer').length, 1);
   });
 });
diff --git a/app/pages/project/components/ProjectNavbar/ProjectNavbar.spec.js b/app/pages/project/components/ProjectNavbar/ProjectNavbar.spec.js
index 3f2468b8..9fee9428 100644
--- a/app/pages/project/components/ProjectNavbar/ProjectNavbar.spec.js
+++ b/app/pages/project/components/ProjectNavbar/ProjectNavbar.spec.js
@@ -3,14 +3,14 @@ import { mount, shallow } from 'enzyme';
 import { expect } from 'chai';
 import sinon from 'sinon';
 import { ProjectNavbar } from './ProjectNavbar';
-import getProjectLinks from './helpers/getProjectLinks';
+import { getProjectLinks } from '../../../../lib/nav-helpers';
 import {
   buildLinksWithLabels,
   projectOwnerUser,
   projectRoles,
   projectWithoutRedirect,
   workflow
-} from './testHelpers';
+} from '../../../../../test';
 
 describe('ProjectNavbar', function() {
   const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: projectOwnerUser });
diff --git a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
index 93ff8e7c..70b50905 100644
--- a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
+++ b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 import apiClient from 'panoptes-client/lib/api-client';
 import isAdmin from '../../../../lib/is-admin';
 
-import getProjectLinks from './helpers/getProjectLinks';
+import { getProjectLinks } from '../../../../lib/nav-helpers';
 import ProjectNavbar from './ProjectNavbar';
 
 class ProjectNavbarContainer extends Component {
@@ -25,45 +25,10 @@ class ProjectNavbarContainer extends Component {
     apiClient.stopListening('change', this.handleClientChange.bind(this));
   }
 
-  handleClientChange() {
-    const adminEnabled = isAdmin();
-    if (adminEnabled !== this.state.adminEnabled) {
-      // Trigger a re-render if an admin user toggles the admin mode checkbox
-      this.setState({ adminEnabled });
-    }
-  }
-
-  getExternalLinks() {
-    const allExternalLinks = _.get(this.props.project, 'urls', [])
-      .map(link => ({ ...link, isExternalLink: true }));
-
-    const partitionedLinks = _.partition(allExternalLinks, link => (link.site));
-
-    const external = partitionedLinks[1].map(link => ({
-      isExternalLink: true,
-      label: link.label,
-      url: link.url
-    }));
-
-    const social = partitionedLinks[0].map(link => ({
-      isExternalLink: true,
-      isSocialLink: true,
-      label: link.label,
-      site: link.site,
-      url: link.url
-    }));
-
-    return {
-      external,
-      social
-    };
-  }
-
   getNavLinks() {
     const project = this.getProjectLinks();
-    const { external, social } = this.getExternalLinks();
     const org = this.getOrganizationLink();
-    return [].concat(project, org, external, social);
+    return [].concat(project, org);
   }
 
   getOrganizationLink() {
@@ -72,7 +37,6 @@ class ProjectNavbarContainer extends Component {
 
     if (organization) {
       link.push({
-        isExternalLink: true,
         label: organization.display_name,
         url: `/organizations/${organization.slug}`
       });
@@ -93,6 +57,15 @@ class ProjectNavbarContainer extends Component {
     }));
   }
 
+
+  handleClientChange() {
+    const adminEnabled = isAdmin();
+    if (adminEnabled !== this.state.adminEnabled) {
+      // Trigger a re-render if an admin user toggles the admin mode checkbox
+      this.setState({ adminEnabled });
+    }
+  }
+
   render() {
     const avatarSrc = _.get(this.props.projectAvatar, 'src', undefined);
     const backgroundSrc = _.get(this.props.background, 'src', undefined);
diff --git a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.spec.js b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.spec.js
index cfd4bf5f..fc4ea71a 100644
--- a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.spec.js
+++ b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.spec.js
@@ -12,11 +12,10 @@ import {
   randomUser,
   translation,
   workflow
-} from './testHelpers';
+} from '../../../../../test';
 
 describe('ProjectNavbarContainer', function() {
   let wrapper;
-  const getExternalLinksSpy = sinon.spy(ProjectNavbarContainer.prototype, 'getExternalLinks');
   const getNavLinksSpy = sinon.spy(ProjectNavbarContainer.prototype, 'getNavLinks');
   const getOrganizationLinkSpy = sinon.spy(ProjectNavbarContainer.prototype, 'getOrganizationLink');
   const getProjectLinksSpy = sinon.spy(ProjectNavbarContainer.prototype, 'getProjectLinks');
@@ -24,7 +23,6 @@ describe('ProjectNavbarContainer', function() {
   before(function() {
     getNavLinksSpy.resetHistory();
     getProjectLinksSpy.resetHistory();
-    getExternalLinksSpy.resetHistory();
     getOrganizationLinkSpy.resetHistory();
 
     wrapper = shallow(
@@ -51,10 +49,6 @@ describe('ProjectNavbarContainer', function() {
     expect(getProjectLinksSpy.calledOnce).to.be.true;
   });
 
-  it('should call getExternalLinks on render', function() {
-    expect(getExternalLinksSpy.calledOnce).to.be.true;
-  });
-
   it('should call getOrganizationLink on render', function() {
     expect(getOrganizationLinkSpy.calledOnce).to.be.true;
   });
diff --git a/app/pages/project/components/ProjectNavbar/components/Avatar/Avatar.spec.js b/app/pages/project/components/ProjectNavbar/components/Avatar/Avatar.spec.js
index 67c66eb9..da949618 100644
--- a/app/pages/project/components/ProjectNavbar/components/Avatar/Avatar.spec.js
+++ b/app/pages/project/components/ProjectNavbar/components/Avatar/Avatar.spec.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { expect } from 'chai';
 import Avatar, { StyledAvatarImg, DefaultProjectAvatarWrapper, StyledZooniverseLogo } from './Avatar';
-import { projectAvatar, projectWithoutRedirect } from '../../testHelpers';
+import { projectAvatar, projectWithoutRedirect } from '../../../../../../../test';
 
 describe('Avatar', function() {
   it('should render without crashing', function() {
diff --git a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.jsx b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.jsx
index 657b8e75..a77ac959 100644
--- a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.jsx
+++ b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.jsx
@@ -1,10 +1,9 @@
-import _ from 'lodash';
 import { Link } from 'react-router';
 import React from 'react';
 import PropTypes from 'prop-types';
 import styled from 'styled-components';
 import { pxToRem } from '../../../../../../theme';
-import socialIcons from '../../socialIcons';
+import ExternalLink from '../../../../../../components/ExternalLinksBlock/components/ExternalLink';
 
 const commonStyles = `
   display: block;
@@ -25,7 +24,7 @@ export const StyledInternalLink = styled(Link).attrs({
   ${commonStyles}
 `;
 
-export const StyledExternalLink = styled.a`
+export const StyledExternalLink = styled(ExternalLink)`
   color: white;
   ${commonStyles}
 `;
@@ -36,54 +35,49 @@ export const StyledLinkPlaceholder = styled.span`
   ${commonStyles}
 `;
 
-function NavLink({ isExternalLink, isSocialLink, label, site, url, ...props }) {
-  let iconClasses = '';
-  const linkProps = _.clone(props);
-  const LinkComponent = (isExternalLink) ? StyledExternalLink : StyledInternalLink;
+function NavLink({ className, disabled, isExternalLink, label, url }) {
+  const linkProps = {
+    className,
+    disabled
+  };
 
-  if (isExternalLink) {
-    iconClasses = 'fa fa-external-link fa-fw';
-    linkProps.target = '_blank';
-    linkProps.rel = 'noopener noreferrer';
-    linkProps.href = url;
-  } else {
-    linkProps.to = url;
-  }
+  const LinkComponent = (disabled) ? StyledLinkPlaceholder : StyledInternalLink;
 
-  if (isSocialLink) {
-    const icon = socialIcons[site].icon;
-    linkProps['aria-label'] = socialIcons[site].label;
-    iconClasses = `fa ${icon} fa-fw`;
+  if (!disabled) {
+    linkProps.to = url;
   }
 
-  if (linkProps.disabled) {
+  if (isExternalLink) {
     return (
-      <StyledLinkPlaceholder {...linkProps}>
-        {label}
-      </StyledLinkPlaceholder>
+      <StyledExternalLink
+        className={className}
+        isExternalLink={isExternalLink}
+        label={label}
+        url={url}
+      />
     );
   }
 
   return (
     <LinkComponent {...linkProps}>
-      {!isSocialLink &&
-        label}
-      {iconClasses && <i className={iconClasses} />}
+      {label}
     </LinkComponent>
   );
 }
 
 NavLink.defaultProps = {
+  className: '',
+  disabled: false,
   isExternalLink: false,
-  isSocialLink: false,
   label: '',
   site: '',
   url: ''
 };
 
 NavLink.propTypes = {
+  className: PropTypes.string,
+  disabled: PropTypes.bool,
   isExternalLink: PropTypes.bool,
-  isSocialLink: PropTypes.bool,
   label: PropTypes.string,
   site: PropTypes.string,
   url: PropTypes.string
diff --git a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
index 2408cc92..63bd8358 100644
--- a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
+++ b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
@@ -10,21 +10,16 @@ import { shallow } from 'enzyme';
 import { expect } from 'chai';
 
 import NavLink from './NavLink';
-import socialIcons from '../../socialIcons';
-import { getProjectLinks } from '../../helpers';
+import { getProjectLinks  } from '../../../../../../lib/nav-helpers';
 import {
   buildLinksWithLabels,
   projectRoles,
   projectWithoutRedirect,
   workflow
-} from '../../testHelpers';
+} from '../../../../../../../test';
 
 const MOCK_EXTERNAL_URL = 'https://www.google.com';
 
-const MOCK_SOCIAL_URL = 'https://www.facebook.com';
-
-const MOCK_SOCIAL_SITE = 'facebook.com/';
-
 describe('NavLink', function() {
   it('renders without crashing', function() {
     shallow(<NavLink />);
@@ -75,58 +70,7 @@ describe('NavLink', function() {
     });
 
     it('should be the StyledExternalLink component', function() {
-      expect(wrapper.name()).to.equal('styled.a');
-    });
-
-    it('should add props to open the url in a new tab', function() {
-      expect(wrapper.props().target).to.equal('_blank');
-      expect(wrapper.props().rel).to.equal('noopener noreferrer');
-    });
-
-    it('should use props.url for the href', function () {
-      expect(wrapper.props().href).to.equal(MOCK_EXTERNAL_URL);
-    });
-
-    it('should have the Font Awesome external link icon', function() {
-      expect(wrapper.find('i').hasClass('fa-external-link')).to.be.true;
-    });
-  });
-
-  describe('when the link is social media', function() {
-    let wrapper;
-    before(function() {
-      wrapper = shallow(
-        <NavLink
-          isExternalLink={true}
-          isSocialLink={true}
-          site={MOCK_SOCIAL_SITE}
-          url={MOCK_SOCIAL_URL}
-        />);
-    });
-
-    it('should be the StyledExternalLink component', function() {
-    });
-
-    it('should add props for the aria-label', function() {
-      expect(wrapper.props()['aria-label']).to.equal(socialIcons[MOCK_SOCIAL_SITE].label);
-    });
-
-    it('should add props to open the url in a new tab', function () {
-      expect(wrapper.props().target).to.equal('_blank');
-      expect(wrapper.props().rel).to.equal('noopener noreferrer');
-    });
-
-    it('should use props.url for the href', function () {
-      expect(wrapper.props().href).to.equal(MOCK_SOCIAL_URL);
-    });
-
-    it('should use the correct font awesome icon', function() {
-      expect(wrapper.find('i').hasClass(socialIcons[MOCK_SOCIAL_SITE].icon)).to.be.true;
-    });
-
-    it('should not use prop.label if defined', function() {
-      wrapper.setProps({ label: 'A label' });
-      expect(wrapper.text().includes('A label')).to.be.false;
+      expect(wrapper.name()).to.equal('Styled(ExternalLink)');
     });
   });
 });
diff --git a/app/pages/project/components/ProjectNavbar/components/ProjectTitle/ProjectTitle.spec.js b/app/pages/project/components/ProjectNavbar/components/ProjectTitle/ProjectTitle.spec.js
index 127ccc5e..0e03ed6a 100644
--- a/app/pages/project/components/ProjectNavbar/components/ProjectTitle/ProjectTitle.spec.js
+++ b/app/pages/project/components/ProjectNavbar/components/ProjectTitle/ProjectTitle.spec.js
@@ -11,7 +11,7 @@ import { expect } from 'chai';
 import ProjectTitle, { H1, StyledLink, StyledRedirect, StyledCheckMark, StyledCheckMarkWrapper, StyledUnderReview } from './ProjectTitle';
 import {
   projectWithoutRedirect, projectWithRedirect
-} from '../../testHelpers';
+} from '../../../../../../../test';
 
 describe('ProjectTitle', function() {
   let wrapper;
diff --git a/app/pages/project/components/ProjectNavbar/socialIcons.json b/app/pages/project/components/ProjectNavbar/socialIcons.json
deleted file mode 100644
index e832b961..00000000
--- a/app/pages/project/components/ProjectNavbar/socialIcons.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-  "bitbucket.com/": {
-    "icon": "fa-bitbucket",
-    "label": "Link to BitBucket repository"
-  },
-  "facebook.com/": {
-    "icon": "fa-facebook-square",
-    "label": "Link to Facebook Page"
-  },
-  "github.com/": {
-    "icon": "fa-github",
-    "label": "Link to GitHub repository"
-  },
-  "pinterest.com/": {
-    "icon": "fa-pinterest",
-    "label": "Link to Pinterest page "
-  },
-  "plus.google.com/": {
-    "icon": "fa-google-plus",
-    "label": "Link to Google+ page"
-  },
-  "reddit.com/": {
-    "icon": "fa-reddit",
-    "label": "Link to Reddit"
-  },
-  "tumblr.com/": {
-    "icon": "fa-tumblr",
-    "label": "Link to Tumblr"
-  },
-  "twitter.com/": {
-    "icon": "fa-twitter",
-    "label": "Link to Twitter profile"
-  },
-  "weibo.com/": {
-    "icon": "fa-weibo",
-    "label": "Link to Weibo page"
-  },
-  "wordpress.com/": {
-    "icon": "fa-wordpress",
-    "label": "Link to WordPress site"
-  },
-  "youtube.com/": {
-    "icon": "fa-youtube",
-    "label": "Link to YouTube video"
-  },
-  "instagram.com/": {
-    "icon": "fa-instagram",
-    "label": "Link to Instagram profile"
-  },
-  "medium.com/": {
-    "icon": "fa-medium",
-    "label": "Link to Medium article"
-  }
-}
diff --git a/app/pages/project/home/project-home.jsx b/app/pages/project/home/project-home.jsx
index 3154448c..b58ad6bd 100644
--- a/app/pages/project/home/project-home.jsx
+++ b/app/pages/project/home/project-home.jsx
@@ -10,7 +10,7 @@ import FinishedBanner from '../finished-banner';
 import ProjectMetadata from './metadata';
 import ProjectHomeWorkflowButtons from './home-workflow-buttons';
 import TalkStatus from './talk-status';
-import ProjectNavbar from '../components/ProjectNavbar';
+import ExternalLinksBlock from '../../../components/ExternalLinksBlock';
 
 const ProjectHomePage = (props) => {
   const avatarSrc = props.researcherAvatar || '/assets/simple-avatar.png';
@@ -130,28 +130,32 @@ const ProjectHomePage = (props) => {
       <div className="project-home-page__container">
         {props.project.researcher_quote && (
           <div className="project-home-page__researcher-words">
-            <h4 className="project-home-page__small-header"><Translate content="project.home.researcher" /></h4>
+            <Translate className="project-home-page__small-header" content="project.home.researcher" component="h4" />
 
-            <div>
-              <img role="presentation" src={avatarSrc} />
-              <span>&quot;{props.translation.researcher_quote}&quot;</span>
+            <div className="researcher-words__wrapper">
+              <img className="researcher-words__avatar" role="presentation" src={avatarSrc} alt="The researcher" />
+              <p className="researcher-words__quote">&quot;{props.translation.researcher_quote}&quot;</p>
             </div>
           </div>)}
 
-        <div className="project-home-page__about-text">
-          <h4 className="project-home-page__small-header">
-            <Translate
-              content="project.home.about"
-              with={{
-                title: props.translation.display_name
-              }}
-            />
-          </h4>
+        <div className="project-home-page__about-text" style={(props.project.researcher_quote && props.project.urls && props.project.urls.length > 0) ? { flexBasis: '33.333%' } : { flexBasis: '66.666%' }}>
+          <Translate
+            className="project-home-page__small-header"
+            content="project.home.about"
+            component="h4"
+            with={{
+              title: props.translation.display_name
+            }}
+          />
           {props.project.introduction &&
             <Markdown project={props.project}>
               {props.translation.introduction}
             </Markdown>}
         </div>
+        <ExternalLinksBlock
+          header={<Translate className="project-home-page__small-header" component="h4" content="project.home.links" />}
+          resource={props.project}
+        />
       </div>
     </div>
   );
@@ -194,7 +198,8 @@ ProjectHomePage.propTypes = {
     introduction: PropTypes.string,
     redirect: PropTypes.string,
     researcher_quote: PropTypes.string,
-    slug: PropTypes.string
+    slug: PropTypes.string,
+    urls: PropTypes.arrayOf(PropTypes.object)
   }).isRequired,
   projectIsComplete: PropTypes.bool.isRequired,
   researcherAvatar: PropTypes.string,
diff --git a/app/pages/project/home/project-home.spec.js b/app/pages/project/home/project-home.spec.js
index cce4db5e..f42ef665 100644
--- a/app/pages/project/home/project-home.spec.js
+++ b/app/pages/project/home/project-home.spec.js
@@ -18,7 +18,15 @@ const project = {
   display_name: 'Identify Animals',
   experimental_tools: [''],
   id: '123',
-  slug: 'zooniverse/identify-animals'
+  slug: 'zooniverse/identify-animals',
+  urls: [
+    {
+      label: '',
+      path: 'findthething',
+      site: 'facebook.com/',
+      url: 'https://www.facebook.com/find-the-thing'
+    }
+  ]
 };
 
 const talkSubjects = [
@@ -217,7 +225,7 @@ describe('ProjectHome', function() {
     });
   });
 
-  describe('when a project does have a researcher quote', function() {
+  describe('when a project has a researcher quote', function() {
     before(function() {
       const projectWithResearcherQuote = Object.assign({}, project, { researcher_quote: 'Important thoughts!' });
       wrapper.setProps({ project: projectWithResearcherQuote });
diff --git a/app/pages/project/project-page.jsx b/app/pages/project/project-page.jsx
index 06ee71e6..0d34c747 100644
--- a/app/pages/project/project-page.jsx
+++ b/app/pages/project/project-page.jsx
@@ -1,6 +1,5 @@
 import PropTypes from 'prop-types';
 import React from 'react';
-import Translate from 'react-translate-component';
 import { Markdown } from 'markdownz';
 import { sugarClient } from 'panoptes-client/lib/sugar';
 import ProjectNavbar from './components/ProjectNavbar';
diff --git a/css/project-page.styl b/css/project-page.styl
index e829a13f..0e9aa65d 100644
--- a/css/project-page.styl
+++ b/css/project-page.styl
@@ -261,32 +261,37 @@
 
   &__researcher-words
     background-color: white
+    box-sizing: border-box
     color: ZOONIVERSE_NAVY
     display: flex
-    flex: 2 0 auto
+    flex: 1 0 33.333%
     flex-direction: column
-    width: 250px
+    min-width: 400px
     padding: 3em 4vw
     position: relative
 
-    div
-      display: flex
-
     h4
       margin: 0 0 1em 6em
 
-    img
-      border-radius: 50%
-      float: left
-      height: 5em
-      margin-right: 1em
-      min-width: 5em
-
-    span
-      font-size: 1.75em
-      font-style: italic
-      overflow: auto
-      word-wrap: break-word
+    .researcher-words__wrapper
+      display: flex
+      flex-wrap: wrap
+      justify-content: center
+      margin-top: 1em
+
+      .researcher-words__avatar
+        border-radius: 50%
+        height: 75px
+        flex: 0 0 75px
+        margin-right: 1em
+        width: 75px
+
+      .researcher-words__quote
+        flex: 1 0 75%
+        font-size: 1.75em
+        font-style: italic
+        margin: 0
+        word-wrap: break-word
 
   @media (min-width: 1000px)
     &__researcher-words:before
@@ -300,11 +305,12 @@
 
   &__about-text
     background-color: #0f7481
+    box-sizing: border-box
     color: white
-    flex: 3 1 auto
-    padding: 3em 3em 4em 3em
+    flex: 1 0 33.333%
+    min-width: 400px
+    padding: 3em 4vw
     text-align: center
-    width: 600px
 
     a
       background: none
@@ -398,7 +404,7 @@
   text-shadow: 1px 1px #333
   justify-content: center
   margin: 0
-  padding: 2em 0em
+  padding: 2em 3vw
   text-align: center
 
 .collection-page-with-project-context,


Test patch:
diff --git a/app/pages/project/components/ProjectNavbar/testHelpers/helperFunctions.js b/test/helperFunctions.js
similarity index 100%
rename from app/pages/project/components/ProjectNavbar/testHelpers/helperFunctions.js
rename to test/helperFunctions.js
diff --git a/app/pages/project/components/ProjectNavbar/testHelpers/index.js b/test/index.js
similarity index 94%
rename from app/pages/project/components/ProjectNavbar/testHelpers/index.js
rename to test/index.js
index 2deb0eaf..1e255550 100644
--- a/app/pages/project/components/ProjectNavbar/testHelpers/index.js
+++ b/test/index.js
@@ -1,6 +1,7 @@
 import {
   adminUser,
   background,
+  externalLinks,
   organization,
   projectAvatar,
   projectOwner,
@@ -21,6 +22,7 @@ export {
   adminUser,
   background,
   buildLinksWithLabels,
+  externalLinks,
   organization,
   projectAvatar,
   projectOwner,
diff --git a/app/pages/project/components/ProjectNavbar/testHelpers/mockObjects.js b/test/mockObjects.js
similarity index 67%
rename from app/pages/project/components/ProjectNavbar/testHelpers/mockObjects.js
rename to test/mockObjects.js
index 36d95823..2da4e9dd 100644
--- a/app/pages/project/components/ProjectNavbar/testHelpers/mockObjects.js
+++ b/test/mockObjects.js
@@ -24,32 +24,31 @@ export const randomUser = { id: '10' };
 
 export const adminUser = { id: '36', admin: true };
 
+export const socialMediaUrl = {
+  label: '',
+  path: 'findthething',
+  site: 'facebook.com/',
+  url: 'https://www.facebook.com/find-the-thing'
+};
+
+export const externalUrl = {
+  label: 'My Blog',
+  site: '',
+  url: 'https://my-blog.com'
+};
+
 export const projectWithoutRedirect = {
   id: '2',
   slug: 'zooniverse/find-the-thing',
   title: 'Find the Thing',
-  urls: [
-    {
-      label: '',
-      path: 'findthething',
-      site: 'facebook.com/',
-      url: 'https://www.facebook.com/find-the-thing'
-    }
-  ]
+  urls: [socialMediaUrl, externalUrl]
 };
 
 export const projectWithRedirect = {
   id: '3',
   redirect: 'https://www.redirected-project.org',
   title: 'Find the Thing',
-  urls: [
-    {
-      label: '',
-      path: 'findthething',
-      site: 'facebook.com/',
-      url: 'https://www.facebook.com/find-the-thing'
-    }
-  ]
+  urls: [socialMediaUrl, externalUrl]
 };
 
 export const workflow = { id: '35' };
@@ -70,3 +69,16 @@ export const projectAvatar = {
 export const translation = {
   display_name: 'translated display name'
 };
+
+export const externalLinks = [
+  {
+    isExternalLink: true,
+    isSocialLink: true,
+    ...socialMediaUrl
+  },
+  {
+    isExternalLink: true,
+    isSocialLink: true,
+    ...externalUrl
+  }
+];