diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js
index 56608f28ba1c0b8f0fb0844a6158cc68f46145f0..605a457a2e29e037c8c720e13d2f0b86a9835695 100644
--- a/app/javascript/mastodon/actions/search.js
+++ b/app/javascript/mastodon/actions/search.js
@@ -135,8 +135,7 @@ export const showSearch = () => ({
   type: SEARCH_SHOW,
 });
 
-export const openURL = routerHistory => (dispatch, getState) => {
-  const value = getState().getIn(['search', 'value']);
+export const openURL = (value, history, onFailure) => (dispatch, getState) => {
   const signedIn = !!getState().getIn(['meta', 'me']);
 
   if (!signedIn) {
@@ -148,15 +147,21 @@ export const openURL = routerHistory => (dispatch, getState) => {
   api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
     if (response.data.accounts?.length > 0) {
       dispatch(importFetchedAccounts(response.data.accounts));
-      routerHistory.push(`/@${response.data.accounts[0].acct}`);
+      history.push(`/@${response.data.accounts[0].acct}`);
     } else if (response.data.statuses?.length > 0) {
       dispatch(importFetchedStatuses(response.data.statuses));
-      routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+      history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+    } else if (onFailure) {
+      onFailure();
     }
 
     dispatch(fetchSearchSuccess(response.data, value));
   }).catch(err => {
     dispatch(fetchSearchFail(err));
+
+    if (onFailure) {
+      onFailure();
+    }
   });
 };
 
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index 72eb7e6b654a856a768b3293cd4736880d503af5..ff8dc5fa9f99d8d5d1f7aa2766fb9c17f6ca32bf 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -80,6 +80,7 @@ class Header extends ImmutablePureComponent {
 
   static contextTypes = {
     identity: PropTypes.object,
+    router: PropTypes.object,
   };
 
   static propTypes = {
@@ -101,11 +102,16 @@ class Header extends ImmutablePureComponent {
     onChangeLanguages: PropTypes.func.isRequired,
     onInteractionModal: PropTypes.func.isRequired,
     onOpenAvatar: PropTypes.func.isRequired,
+    onOpenURL: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     domain: PropTypes.string.isRequired,
     hidden: PropTypes.bool,
   };
 
+  setRef = c => {
+    this.node = c;
+  };
+
   openEditProfile = () => {
     window.open('/settings/profile', '_blank');
   };
@@ -162,6 +168,61 @@ class Header extends ImmutablePureComponent {
     });
   };
 
+  handleHashtagClick = e => {
+    const { router } = this.context;
+    const value = e.currentTarget.textContent.replace(/^#/, '');
+
+    if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      router.history.push(`/tags/${value}`);
+    }
+  };
+
+  handleMentionClick = e => {
+    const { router } = this.context;
+    const { onOpenURL } = this.props;
+
+    if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+
+      const link = e.currentTarget;
+
+      onOpenURL(link.href, router.history, () => {
+        window.location = link.href;
+      });
+    }
+  };
+
+  _attachLinkEvents () {
+    const node = this.node;
+
+    if (!node) {
+      return;
+    }
+
+    const links = node.querySelectorAll('a');
+
+    let link;
+
+    for (var i = 0; i < links.length; ++i) {
+      link = links[i];
+
+      if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
+        link.addEventListener('click', this.handleHashtagClick, false);
+      } else if (link.classList.contains('mention')) {
+        link.addEventListener('click', this.handleMentionClick, false);
+      }
+    }
+  }
+
+  componentDidMount () {
+    this._attachLinkEvents();
+  }
+
+  componentDidUpdate () {
+    this._attachLinkEvents();
+  }
+
   render () {
     const { account, hidden, intl, domain } = this.props;
     const { signedIn, permissions } = this.context.identity;
@@ -360,7 +421,7 @@ class Header extends ImmutablePureComponent {
 
           {!(suspended || hidden) && (
             <div className='account__header__extra'>
-              <div className='account__header__bio'>
+              <div className='account__header__bio' ref={this.setRef}>
                 {(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
 
                 {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.jsx b/app/javascript/mastodon/features/account_timeline/components/header.jsx
index c008f0342dbc3937b39b7d9d31594582170e87ee..e64deaefa6f87ad5120f586fe8ae0d90e948d7bd 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.jsx
+++ b/app/javascript/mastodon/features/account_timeline/components/header.jsx
@@ -26,6 +26,7 @@ export default class Header extends ImmutablePureComponent {
     onChangeLanguages: PropTypes.func.isRequired,
     onInteractionModal: PropTypes.func.isRequired,
     onOpenAvatar: PropTypes.func.isRequired,
+    onOpenURL: PropTypes.func.isRequired,
     hideTabs: PropTypes.bool,
     domain: PropTypes.string.isRequired,
     hidden: PropTypes.bool,
@@ -137,6 +138,7 @@ export default class Header extends ImmutablePureComponent {
           onChangeLanguages={this.handleChangeLanguages}
           onInteractionModal={this.handleInteractionModal}
           onOpenAvatar={this.handleOpenAvatar}
+          onOpenURL={this.props.onOpenURL}
           domain={this.props.domain}
           hidden={hidden}
         />
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
index f53cd248079d5dbc8f66cd5fbe64d47ec9a65799..419a9fa566443d4b87312fbcae3f220cc0793c60 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
@@ -10,6 +10,7 @@ import {
   pinAccount,
   unpinAccount,
 } from '../../../actions/accounts';
+import { openURL } from 'mastodon/actions/search';
 import {
   mentionCompose,
   directCompose,
@@ -159,6 +160,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }));
   },
 
+  onOpenURL (url, routerHistory, onFailure) {
+    dispatch(openURL(url, routerHistory, onFailure));
+  },
+
 });
 
 export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx
index 46723f5cce360709feb42fc3b4ec1fcde01407ae..7fff587d689ddbe13909cd603e4f2a5eab0e9010 100644
--- a/app/javascript/mastodon/features/compose/components/search.jsx
+++ b/app/javascript/mastodon/features/compose/components/search.jsx
@@ -161,9 +161,9 @@ class Search extends React.PureComponent {
 
   handleURLClick = () => {
     const { router } = this.context;
-    const { onOpenURL } = this.props;
+    const { value, onOpenURL } = this.props;
 
-    onOpenURL(router.history);
+    onOpenURL(value, router.history);
   };
 
   handleStatusSearch = () => {
diff --git a/app/javascript/mastodon/features/compose/containers/search_container.js b/app/javascript/mastodon/features/compose/containers/search_container.js
index 3ee55fae598a552f6add468ad41dbedcd67ba829..3d2d728c8d27f7aa13d9665a34991c55652769cf 100644
--- a/app/javascript/mastodon/features/compose/containers/search_container.js
+++ b/app/javascript/mastodon/features/compose/containers/search_container.js
@@ -34,8 +34,8 @@ const mapDispatchToProps = dispatch => ({
     dispatch(showSearch());
   },
 
-  onOpenURL (routerHistory) {
-    dispatch(openURL(routerHistory));
+  onOpenURL (q, routerHistory) {
+    dispatch(openURL(q, routerHistory));
   },
 
   onClickSearchResult (q, type) {