From dd512c140bee24446d1ddab901d04b51a72a2d86 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 14 Dec 2020 01:41:13 +0100
Subject: [PATCH] WIP

---
 app/controllers/account_follow_controller.rb  | 12 ---
 .../account_unfollow_controller.rb            | 12 ---
 app/controllers/accounts_controller.rb        | 24 +-----
 .../api/v1/accounts/lookup_controller.rb      | 16 ++++
 .../authorize_interactions_controller.rb      |  2 +-
 .../concerns/account_controller_concern.rb    |  7 --
 .../concerns/web_app_controller_concern.rb    | 18 +++++
 app/controllers/directories_controller.rb     | 28 +------
 .../follower_accounts_controller.rb           |  9 ++-
 .../following_accounts_controller.rb          |  9 ++-
 app/controllers/home_controller.rb            | 43 +----------
 .../public_timelines_controller.rb            | 18 ++---
 app/controllers/statuses_controller.rb        | 18 +----
 app/controllers/tags_controller.rb            | 15 +---
 app/helpers/application_helper.rb             | 14 ++--
 app/javascript/mastodon/actions/accounts.js   | 32 ++++++++
 app/javascript/mastodon/actions/compose.js    |  4 +-
 app/javascript/mastodon/components/account.js |  2 +-
 app/javascript/mastodon/components/hashtag.js |  2 +-
 app/javascript/mastodon/components/status.js  | 64 ++++++++--------
 .../mastodon/components/status_action_bar.js  |  2 +-
 .../mastodon/components/status_content.js     |  6 +-
 .../mastodon/containers/mastodon.js           |  6 +-
 .../features/account/components/header.js     |  6 +-
 .../features/account_gallery/index.js         | 64 +++++++++++-----
 .../account_timeline/components/header.js     |  6 +-
 .../account_timeline/components/moved_note.js |  2 +-
 .../features/account_timeline/index.js        | 55 +++++++++-----
 .../compose/components/navigation_bar.js      |  4 +-
 .../compose/components/reply_indicator.js     |  2 +-
 .../mastodon/features/compose/index.js        |  6 +-
 .../components/conversation.js                |  2 +-
 .../directory/components/account_card.js      |  2 +-
 .../components/account_authorize.js           |  2 +-
 .../mastodon/features/followers/index.js      | 66 ++++++++++++-----
 .../mastodon/features/following/index.js      | 66 ++++++++++++-----
 .../components/announcements.js               |  6 +-
 .../features/getting_started/index.js         | 14 ++--
 .../mastodon/features/home_timeline/index.js  |  2 +-
 .../mastodon/features/lists/index.js          |  2 +-
 .../components/follow_request.js              |  2 +-
 .../notifications/components/notification.js  |  6 +-
 .../picture_in_picture/components/footer.js   |  2 +-
 .../picture_in_picture/components/header.js   |  2 +-
 .../status/components/detailed_status.js      |  6 +-
 .../mastodon/features/status/index.js         |  2 +-
 .../features/ui/components/boost_modal.js     |  2 +-
 .../features/ui/components/columns_area.js    |  2 +-
 .../features/ui/components/compose_panel.js   |  7 +-
 .../features/ui/components/list_panel.js      |  2 +-
 .../features/ui/components/media_modal.js     |  7 --
 .../ui/components/navigation_panel.js         | 10 +--
 .../features/ui/components/tabs_bar.js        |  6 +-
 app/javascript/mastodon/features/ui/index.js  | 50 ++++++-------
 app/javascript/mastodon/main.js               |  6 +-
 .../mastodon/reducers/accounts_map.js         | 15 ++++
 app/javascript/mastodon/reducers/index.js     |  2 +
 .../service_worker/web_push_notifications.js  |  2 +-
 app/views/accounts/show.html.haml             | 74 ++-----------------
 .../_post_follow_actions.html.haml            |  2 +-
 app/views/directories/index.html.haml         | 49 ++----------
 app/views/follower_accounts/index.html.haml   | 19 ++---
 app/views/following_accounts/index.html.haml  | 19 ++---
 .../notification_mailer/_status.html.haml     |  2 +-
 .../notification_mailer/_status.text.erb      |  2 +-
 .../notification_mailer/digest.html.haml      |  2 +-
 app/views/notification_mailer/digest.text.erb |  2 +-
 .../notification_mailer/favourite.html.haml   |  2 +-
 .../notification_mailer/follow.html.haml      |  2 +-
 app/views/notification_mailer/follow.text.erb |  2 +-
 .../follow_request.html.haml                  |  2 +-
 .../follow_request.text.erb                   |  2 +-
 .../notification_mailer/mention.html.haml     |  2 +-
 .../notification_mailer/reblog.html.haml      |  2 +-
 app/views/public_timelines/show.html.haml     | 18 ++---
 app/views/statuses/_detailed_status.html.haml |  2 +-
 app/views/statuses/show.html.haml             | 16 ++--
 app/views/tags/show.html.haml                 | 15 ++--
 app/views/user_mailer/welcome.html.haml       |  2 +-
 app/views/user_mailer/welcome.text.erb        |  2 +-
 config/routes.rb                              | 30 +++++++-
 81 files changed, 517 insertions(+), 554 deletions(-)
 delete mode 100644 app/controllers/account_follow_controller.rb
 delete mode 100644 app/controllers/account_unfollow_controller.rb
 create mode 100644 app/controllers/api/v1/accounts/lookup_controller.rb
 create mode 100644 app/controllers/concerns/web_app_controller_concern.rb
 create mode 100644 app/javascript/mastodon/reducers/accounts_map.js

diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb
deleted file mode 100644
index 33394074db4..00000000000
--- a/app/controllers/account_follow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountFollowController < ApplicationController
-  include AccountControllerConcern
-
-  before_action :authenticate_user!
-
-  def create
-    FollowService.new.call(current_user.account, @account, with_rate_limit: true)
-    redirect_to account_path(@account)
-  end
-end
diff --git a/app/controllers/account_unfollow_controller.rb b/app/controllers/account_unfollow_controller.rb
deleted file mode 100644
index 378ec86dc62..00000000000
--- a/app/controllers/account_unfollow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountUnfollowController < ApplicationController
-  include AccountControllerConcern
-
-  before_action :authenticate_user!
-
-  def create
-    UnfollowService.new.call(current_user.account, @account)
-    redirect_to account_path(@account)
-  end
-end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index b902ada090a..4ca37b9efa7 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -5,11 +5,11 @@ class AccountsController < ApplicationController
   PAGE_SIZE_MAX = 200
 
   include AccountControllerConcern
+  include WebAppControllerConcern
   include SignatureAuthentication
 
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :set_cache_headers
-  before_action :set_body_classes
 
   skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
   skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -18,24 +18,6 @@ class AccountsController < ApplicationController
     respond_to do |format|
       format.html do
         expires_in 0, public: true unless user_signed_in?
-
-        @pinned_statuses   = []
-        @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
-        @featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
-
-        if current_account && @account.blocking?(current_account)
-          @statuses = []
-          return
-        end
-
-        @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
-        @statuses        = cached_filtered_status_page
-        @rss_url         = rss_url
-
-        unless @statuses.empty?
-          @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
-          @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
-        end
       end
 
       format.rss do
@@ -56,10 +38,6 @@ class AccountsController < ApplicationController
 
   private
 
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
   def show_pinned_statuses?
     [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
   end
diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb
new file mode 100644
index 00000000000..aee6be18a9e
--- /dev/null
+++ b/app/controllers/api/v1/accounts/lookup_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::LookupController < Api::BaseController
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }
+  before_action :set_account
+
+  def show
+    render json: @account, serializer: REST::AccountSerializer
+  end
+
+  private
+
+  def set_account
+    @account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
+  end
+end
diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb
index 29c0288d09d..1d519c96f42 100644
--- a/app/controllers/authorize_interactions_controller.rb
+++ b/app/controllers/authorize_interactions_controller.rb
@@ -13,7 +13,7 @@ class AuthorizeInteractionsController < ApplicationController
     if @resource.is_a?(Account)
       render :show
     elsif @resource.is_a?(Status)
-      redirect_to web_url("statuses/#{@resource.id}")
+      redirect_to short_account_status_path(@resource.account.acct, @resource.id)
     else
       render :error
     end
diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index 11eac0eb6bf..f05fa3f91fb 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -8,18 +8,11 @@ module AccountControllerConcern
   FOLLOW_PER_PAGE = 12
 
   included do
-    layout 'public'
-
-    before_action :set_instance_presenter
     before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
   end
 
   private
 
-  def set_instance_presenter
-    @instance_presenter = InstancePresenter.new
-  end
-
   def set_link_headers
     response.headers['Link'] = LinkHeader.new(
       [
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
new file mode 100644
index 00000000000..8a6c73af3e4
--- /dev/null
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module WebAppControllerConcern
+  extend ActiveSupport::Concern
+
+  included do
+    before_action :set_body_classes
+    before_action :set_referrer_policy_header
+  end
+
+  def set_body_classes
+    @body_classes = 'app-body'
+  end
+
+  def set_referrer_policy_header
+    response.headers['Referrer-Policy'] = 'origin'
+  end
+end
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index f198ad5ba5b..cfd0fa65686 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -1,42 +1,20 @@
 # frozen_string_literal: true
 
 class DirectoriesController < ApplicationController
-  layout 'public'
+  include WebAppControllerConcern
 
   before_action :authenticate_user!, if: :whitelist_mode?
   before_action :require_enabled!
-  before_action :set_instance_presenter
-  before_action :set_tag, only: :show
-  before_action :set_accounts
 
   skip_before_action :require_functional!, unless: :whitelist_mode?
 
   def index
-    render :index
-  end
-
-  def show
-    render :index
+    expires_in 0, public: true if current_account.nil?
   end
 
   private
 
   def require_enabled!
-    return not_found unless Setting.profile_directory
-  end
-
-  def set_tag
-    @tag = Tag.discoverable.find_normalized!(params[:id])
-  end
-
-  def set_accounts
-    @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
-      query.merge!(Account.tagged_with(@tag.id)) if @tag
-      query.merge!(Account.not_excluded_by_account(current_account)) if current_account
-    end
-  end
-
-  def set_instance_presenter
-    @instance_presenter = InstancePresenter.new
+    not_found unless Setting.profile_directory
   end
 end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index ff4df2adfca..ebdefcf9b5b 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -2,6 +2,7 @@
 
 class FollowerAccountsController < ApplicationController
   include AccountControllerConcern
+  include WebAppControllerConcern
   include SignatureVerification
 
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
@@ -14,10 +15,6 @@ class FollowerAccountsController < ApplicationController
     respond_to do |format|
       format.html do
         expires_in 0, public: true unless user_signed_in?
-
-        next if @account.user_hides_network?
-
-        follows
       end
 
       format.json do
@@ -36,6 +33,10 @@ class FollowerAccountsController < ApplicationController
 
   private
 
+  def username_param
+    params[:username] || params[:account_username]
+  end
+
   def follows
     return @follows if defined?(@follows)
 
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 6bb95c45498..4500fb5d7e2 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -2,6 +2,7 @@
 
 class FollowingAccountsController < ApplicationController
   include AccountControllerConcern
+  include WebAppControllerConcern
   include SignatureVerification
 
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
@@ -14,10 +15,6 @@ class FollowingAccountsController < ApplicationController
     respond_to do |format|
       format.html do
         expires_in 0, public: true unless user_signed_in?
-
-        next if @account.user_hides_network?
-
-        follows
       end
 
       format.json do
@@ -36,6 +33,10 @@ class FollowingAccountsController < ApplicationController
 
   private
 
+  def username_param
+    params[:username] || params[:account_username]
+  end
+
   def follows
     return @follows if defined?(@follows)
 
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 702889cd030..72573d231da 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,47 +1,16 @@
 # frozen_string_literal: true
 
 class HomeController < ApplicationController
-  before_action :redirect_unauthenticated_to_permalinks!
   before_action :authenticate_user!
-  before_action :set_referrer_policy_header
 
-  def index
-    @body_classes = 'app-body'
-  end
-
-  private
-
-  def redirect_unauthenticated_to_permalinks!
-    return if user_signed_in?
+  include WebAppControllerConcern
 
-    matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
+  def index; end
 
-    if matches
-      case matches[1]
-      when 'statuses'
-        status = Status.find_by(id: matches[2])
-
-        if status&.distributable?
-          redirect_to(ActivityPub::TagManager.instance.url_for(status))
-          return
-        end
-      when 'accounts'
-        account = Account.find_by(id: matches[2])
-
-        if account
-          redirect_to(ActivityPub::TagManager.instance.url_for(account))
-          return
-        end
-      end
-    end
-
-    matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
-
-    redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
-  end
+  private
 
   def default_redirect_path
-    if request.path.start_with?('/web') || whitelist_mode?
+    if whitelist_mode?
       new_user_session_path
     elsif single_user_mode?
       short_account_path(Account.local.without_suspended.where('id > 0').first)
@@ -49,8 +18,4 @@ class HomeController < ApplicationController
       about_path
     end
   end
-
-  def set_referrer_policy_header
-    response.headers['Referrer-Policy'] = 'origin'
-  end
 end
diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb
index 1332ba16c2b..1fcb9fbca9e 100644
--- a/app/controllers/public_timelines_controller.rb
+++ b/app/controllers/public_timelines_controller.rb
@@ -1,26 +1,18 @@
 # frozen_string_literal: true
 
 class PublicTimelinesController < ApplicationController
-  layout 'public'
+  include WebAppControllerConcern
 
   before_action :authenticate_user!, if: :whitelist_mode?
   before_action :require_enabled!
-  before_action :set_body_classes
-  before_action :set_instance_presenter
 
-  def show; end
+  def show
+    expires_in 0, public: true if current_account.nil?
+  end
 
   private
 
   def require_enabled!
-    not_found unless Setting.timeline_preview
-  end
-
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
-  def set_instance_presenter
-    @instance_presenter = InstancePresenter.new
+    not_found unless user_signed_in? || Setting.timeline_preview
   end
 end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 87612a29662..a55b40103d6 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,21 +1,17 @@
 # frozen_string_literal: true
 
 class StatusesController < ApplicationController
-  include StatusControllerConcern
   include SignatureAuthentication
   include Authorization
   include AccountOwnedConcern
-
-  layout 'public'
+  include WebAppControllerConcern
 
   before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :set_status
-  before_action :set_instance_presenter
   before_action :set_link_headers
   before_action :redirect_to_original, only: :show
   before_action :set_referrer_policy_header, only: :show
   before_action :set_cache_headers
-  before_action :set_body_classes
   before_action :set_autoplay, only: :embed
 
   skip_around_action :set_locale, if: -> { request.format == :json }
@@ -29,8 +25,6 @@ class StatusesController < ApplicationController
     respond_to do |format|
       format.html do
         expires_in 10.seconds, public: true if current_account.nil?
-        set_ancestors
-        set_descendants
       end
 
       format.json do
@@ -56,10 +50,6 @@ class StatusesController < ApplicationController
 
   private
 
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
   def set_link_headers
     response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
   end
@@ -71,16 +61,12 @@ class StatusesController < ApplicationController
     not_found
   end
 
-  def set_instance_presenter
-    @instance_presenter = InstancePresenter.new
-  end
-
   def redirect_to_original
     redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
   end
 
   def set_referrer_policy_header
-    response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
+    response.headers['Referrer-Policy'] = 'origin'
   end
 
   def set_autoplay
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 6616ba107c8..aa3969464cf 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -2,26 +2,23 @@
 
 class TagsController < ApplicationController
   include SignatureVerification
+  include WebAppControllerConcern
 
   PAGE_SIZE     = 20
   PAGE_SIZE_MAX = 200
 
-  layout 'public'
-
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :authenticate_user!, if: :whitelist_mode?
   before_action :set_local
   before_action :set_tag
   before_action :set_statuses
-  before_action :set_body_classes
-  before_action :set_instance_presenter
 
   skip_before_action :require_functional!, unless: :whitelist_mode?
 
   def show
     respond_to do |format|
       format.html do
-        expires_in 0, public: true
+        expires_in 0, public: true if current_account.nil?
       end
 
       format.rss do
@@ -55,14 +52,6 @@ class TagsController < ApplicationController
     end
   end
 
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
-  def set_instance_presenter
-    @instance_presenter = InstancePresenter.new
-  end
-
   def limit_param
     params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
   end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bf5742d34f2..31bcff56827 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -158,19 +158,15 @@ module ApplicationHelper
 
   def render_initial_state
     state_params = {
-      settings: {
-        known_fediverse: Setting.show_known_fediverse_at_about_page,
-      },
-
       text: [params[:title], params[:text], params[:url]].compact.join(' '),
     }
 
-    permit_visibilities = %w(public unlisted private direct)
-    default_privacy     = current_account&.user&.setting_default_privacy
-    permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
-    state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
-
     if user_signed_in?
+      permit_visibilities = %w(public unlisted private direct)
+      default_privacy     = current_account&.user&.setting_default_privacy
+      permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
+
+      state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
       state_params[:settings]          = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
       state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
       state_params[:current_account]   = current_account
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 58b63660260..ce7bb6d5f09 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
 export const ACCOUNT_FETCH_FAIL    = 'ACCOUNT_FETCH_FAIL';
 
+export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
+export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
+export const ACCOUNT_LOOKUP_FAIL    = 'ACCOUNT_LOOKUP_FAIL';
+
 export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
 export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
 export const ACCOUNT_FOLLOW_FAIL    = 'ACCOUNT_FOLLOW_FAIL';
@@ -87,6 +91,34 @@ export function fetchAccount(id) {
   };
 };
 
+export const lookupAccount = acct => (dispatch, getState) => {
+  dispatch(lookupAccountRequest(acct));
+
+  api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
+    dispatch(fetchRelationships([response.data.id]));
+    dispatch(importFetchedAccount(response.data));
+    dispatch(lookupAccountSuccess());
+  }).catch(error => {
+    dispatch(lookupAccountFail(acct, error));
+  });
+};
+
+export const lookupAccountRequest = (acct) => ({
+  type: ACCOUNT_LOOKUP_REQUEST,
+  acct,
+});
+
+export const lookupAccountSuccess = () => ({
+  type: ACCOUNT_LOOKUP_SUCCESS,
+});
+
+export const lookupAccountFail = (acct, error) => ({
+  type: ACCOUNT_LOOKUP_FAIL,
+  acct,
+  error,
+  skipAlert: true,
+});
+
 export function fetchAccountRequest(id) {
   return {
     type: ACCOUNT_FETCH_REQUEST,
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 891403969e2..2b2f1787e26 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -72,7 +72,7 @@ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
 
 export const ensureComposeIsVisible = (getState, routerHistory) => {
   if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
-    routerHistory.push('/statuses/new');
+    routerHistory.push('/publish');
   }
 };
 
@@ -152,7 +152,7 @@ export function submitCompose(routerHistory) {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
       },
     }).then(function (response) {
-      if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
+      if (routerHistory && routerHistory.location.pathname === '/publish' && window.history.state) {
         routerHistory.goBack();
       }
 
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 0e40ee1d6a5..733192305e4 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -116,7 +116,7 @@ class Account extends ImmutablePureComponent {
     return (
       <div className='account'>
         <div className='account__wrapper'>
-          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
+          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
             <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
             {mute_expires_at}
             <DisplayName account={account} />
diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js
index d766ca90d78..c23a4674d77 100644
--- a/app/javascript/mastodon/components/hashtag.js
+++ b/app/javascript/mastodon/components/hashtag.js
@@ -27,7 +27,7 @@ const Hashtag = ({ hashtag }) => (
     <div className='trends__item__name'>
       <Permalink
         href={hashtag.get('url')}
-        to={`/timelines/tag/${hashtag.get('name')}`}
+        to={`/tags/${hashtag.get('name')}`}
       >
         #<span>{hashtag.get('name')}</span>
       </Permalink>
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 295e83f5819..52672e02e74 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -134,42 +134,28 @@ class Status extends ImmutablePureComponent {
     this.setState({ showMedia: !this.state.showMedia });
   }
 
-  handleClick = () => {
-    if (this.props.onClick) {
-      this.props.onClick();
+  handleClick = e => {
+    if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
       return;
     }
 
-    if (!this.context.router) {
-      return;
+    if (e) {
+      e.preventDefault();
     }
 
-    const { status } = this.props;
-    this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
+    this.handleHotkeyOpen();
   }
 
-  handleExpandClick = (e) => {
-    if (this.props.onClick) {
-      this.props.onClick();
+  handleAccountClick = e => {
+    if (e && (e.button !== 0 || e.ctrlKey || e.metaKey))  {
       return;
     }
 
-    if (e.button === 0) {
-      if (!this.context.router) {
-        return;
-      }
-
-      const { status } = this.props;
-      this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
-    }
-  }
-
-  handleAccountClick = (e) => {
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      const id = e.currentTarget.getAttribute('data-id');
+    if (e) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${id}`);
     }
+
+    this.handleHotkeyOpenProfile();
   }
 
   handleExpandedToggle = () => {
@@ -242,11 +228,30 @@ class Status extends ImmutablePureComponent {
   }
 
   handleHotkeyOpen = () => {
-    this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
+    if (this.props.onClick) {
+      this.props.onClick();
+      return;
+    }
+
+    const { router } = this.context;
+    const status = this._properStatus();
+
+    if (!router) {
+      return;
+    }
+
+    router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
   }
 
   handleHotkeyOpenProfile = () => {
-    this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
+    const { router } = this.context;
+    const status = this._properStatus();
+
+    if (!router) {
+      return;
+    }
+
+    router.history.push(`/@${status.getIn(['account', 'acct'])}`);
   }
 
   handleHotkeyMoveUp = e => {
@@ -465,14 +470,15 @@ class Status extends ImmutablePureComponent {
           {prepend}
 
           <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
-            <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
+            <div className='status__expand' onClick={this.handleClick} role='presentation' />
+
             <div className='status__info'>
-              <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+              <a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
                 <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
                 <RelativeTimestamp timestamp={status.get('created_at')} />
               </a>
 
-              <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
+              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
                 <div className='status__avatar'>
                   {statusAvatar}
                 </div>
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 9981f2449bf..85c76edee4b 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -186,7 +186,7 @@ class StatusActionBar extends ImmutablePureComponent {
   }
 
   handleOpen = () => {
-    this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
+    this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
   }
 
   handleEmbed = () => {
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 35bd505142e..b3d7942801e 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
   onMentionClick = (mention, e) => {
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${mention.get('id')}`);
+      this.context.router.history.push(`/@${mention.get('acct')}`);
     }
   }
 
@@ -121,7 +121,7 @@ export default class StatusContent extends React.PureComponent {
 
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/timelines/tag/${hashtag}`);
+      this.context.router.history.push(`/tags/${hashtag}`);
     }
   }
 
@@ -202,7 +202,7 @@ export default class StatusContent extends React.PureComponent {
       let mentionsPlaceholder = '';
 
       const mentionLinks = status.get('mentions').map(item => (
-        <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'>
+        <Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention'>
           @<span>{item.get('username')}</span>
         </Permalink>
       )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 3ac58cf7c5e..3dcbe792381 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -14,7 +14,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import { previewState as previewMediaState } from 'mastodon/features/ui/components/media_modal';
 import { previewState as previewVideoState } from 'mastodon/features/ui/components/video_modal';
-import initialState from '../initial_state';
+import initialState, { me } from '../initial_state';
 import ErrorBoundary from '../components/error_boundary';
 
 const { localeData, messages } = getLocale();
@@ -27,7 +27,7 @@ store.dispatch(hydrateAction);
 store.dispatch(fetchCustomEmojis());
 
 const mapStateToProps = state => ({
-  showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
+  showIntroduction: me && state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
 });
 
 @connect(mapStateToProps)
@@ -49,7 +49,7 @@ class MastodonMount extends React.PureComponent {
     }
 
     return (
-      <BrowserRouter basename='/web'>
+      <BrowserRouter>
         <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
           <Route path='/' component={UI} />
         </ScrollContext>
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 8e49486bdaa..cf21a8d13fe 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -330,21 +330,21 @@ class Header extends ImmutablePureComponent {
 
             {!suspended && (
               <div className='account__header__extra__links'>
-                <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
+                <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
                   <ShortNumber
                     value={account.get('statuses_count')}
                     renderer={counterRenderer('statuses')}
                   />
                 </NavLink>
 
-                <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
+                <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
                   <ShortNumber
                     value={account.get('following_count')}
                     renderer={counterRenderer('following')}
                   />
                 </NavLink>
 
-                <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
+                <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
                   <ShortNumber
                     value={account.get('followers_count')}
                     renderer={counterRenderer('followers')}
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index 015a6a6d706..47764109899 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from 'mastodon/actions/accounts';
+import { lookupAccount } from 'mastodon/actions/accounts';
 import { expandAccountMediaTimeline } from '../../actions/timelines';
 import LoadingIndicator from 'mastodon/components/loading_indicator';
 import Column from '../ui/components/column';
@@ -17,14 +17,25 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
 import { openModal } from 'mastodon/actions/modal';
 import { FormattedMessage } from 'react-intl';
 
-const mapStateToProps = (state, props) => ({
-  isAccount: !!state.getIn(['accounts', props.params.accountId]),
-  attachments: getAccountGallery(state, props.params.accountId),
-  isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
-  hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
-  suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
-  blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
-});
+const mapStateToProps = (state, { params: { acct } }) => {
+  const accountId = state.getIn(['accounts_map', acct]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    isAccount: !!state.getIn(['accounts', accountId]),
+    attachments: getAccountGallery(state, accountId),
+    isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
+    hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
+    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
+    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
+  };
+};
 
 class LoadMoreMedia extends ImmutablePureComponent {
 
@@ -53,7 +64,10 @@ export default @connect(mapStateToProps)
 class AccountGallery extends ImmutablePureComponent {
 
   static propTypes = {
-    params: PropTypes.object.isRequired,
+    params: PropTypes.shape({
+      acct: PropTypes.string.isRequired,
+    }).isRequired,
+    accountId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     attachments: ImmutablePropTypes.list.isRequired,
     isLoading: PropTypes.bool,
@@ -68,15 +82,29 @@ class AccountGallery extends ImmutablePureComponent {
     width: 323,
   };
 
+  _load () {
+    const { accountId, dispatch } = this.props;
+
+    dispatch(expandAccountMediaTimeline(accountId));
+  }
+
   componentDidMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
   }
 
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(nextProps.params.accountId));
-      this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
     }
   }
 
@@ -96,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
+    this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
   };
 
   handleLoadOlder = e => {
@@ -166,7 +194,7 @@ class AccountGallery extends ImmutablePureComponent {
 
         <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
           <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
-            <HeaderContainer accountId={this.props.params.accountId} />
+            <HeaderContainer accountId={this.props.accountId} />
 
             {(suspended || blockedBy) ? (
               <div className='empty-column-indicator'>
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 6b52defe4a0..17b693600e5 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -123,9 +123,9 @@ export default class Header extends ImmutablePureComponent {
 
         {!hideTabs && (
           <div className='account__section-headline'>
-            <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
-            <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink>
-            <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+            <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
+            <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink>
+            <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
           </div>
         )}
       </div>
diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
index 3e090bb5f2b..2e32d660f84 100644
--- a/app/javascript/mastodon/features/account_timeline/components/moved_note.js
+++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
@@ -21,7 +21,7 @@ export default class MovedNote extends ImmutablePureComponent {
   handleAccountClick = e => {
     if (e.button === 0) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
+      this.context.router.history.push(`/@${this.props.to.get('acct')}`);
     }
 
     e.stopPropagation();
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index fa4239d6f5f..c5eeec0837a 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from '../../actions/accounts';
+import { lookupAccount, fetchAccount } from '../../actions/accounts';
 import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
@@ -20,10 +20,19 @@ import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines'
 
 const emptyList = ImmutableList();
 
-const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
+const mapStateToProps = (state, { params: { acct }, withReplies = false }) => {
+  const accountId = state.getIn(['accounts_map', acct]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
   const path = withReplies ? `${accountId}:with_replies` : accountId;
 
   return {
+    accountId,
     remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
     remoteUrl: state.getIn(['accounts', accountId, 'url']),
     isAccount: !!state.getIn(['accounts', accountId]),
@@ -48,7 +57,10 @@ export default @connect(mapStateToProps)
 class AccountTimeline extends ImmutablePureComponent {
 
   static propTypes = {
-    params: PropTypes.object.isRequired,
+    params: PropTypes.shape({
+      acct: PropTypes.string.isRequired,
+    }).isRequired,
+    accountId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
     statusIds: ImmutablePropTypes.list,
@@ -64,8 +76,8 @@ class AccountTimeline extends ImmutablePureComponent {
     multiColumn: PropTypes.bool,
   };
 
-  componentWillMount () {
-    const { params: { accountId }, withReplies, dispatch } = this.props;
+  _load () {
+    const { accountId, withReplies, dispatch } = this.props;
 
     dispatch(fetchAccount(accountId));
     dispatch(fetchAccountIdentityProofs(accountId));
@@ -81,29 +93,32 @@ class AccountTimeline extends ImmutablePureComponent {
     }
   }
 
-  componentWillReceiveProps (nextProps) {
-    const { dispatch } = this.props;
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
 
-    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
-      dispatch(fetchAccount(nextProps.params.accountId));
-      dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
+  }
 
-      if (!nextProps.withReplies) {
-        dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
-      }
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
 
-      dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
     }
 
-    if (nextProps.params.accountId === me && this.props.params.accountId !== me) {
-      dispatch(connectTimeline(`account:${me}`));
-    } else if (this.props.params.accountId === me && nextProps.params.accountId !== me) {
+    if (prevProps.accountId === me && accountId !== me) {
       dispatch(disconnectTimeline(`account:${me}`));
     }
   }
 
   componentWillUnmount () {
-    const { dispatch, params: { accountId } } = this.props;
+    const { dispatch, accountId } = this.props;
 
     if (accountId === me) {
       dispatch(disconnectTimeline(`account:${me}`));
@@ -111,7 +126,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
+    this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
   }
 
   render () {
@@ -153,7 +168,7 @@ class AccountTimeline extends ImmutablePureComponent {
         <ColumnBackButton multiColumn={multiColumn} />
 
         <StatusList
-          prepend={<HeaderContainer accountId={this.props.params.accountId} />}
+          prepend={<HeaderContainer accountId={this.props.accountId} />}
           alwaysPrepend
           append={remoteMessage}
           scrollKey='account_timeline'
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 840d0a3da3a..e6ba7d8b73d 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -19,13 +19,13 @@ export default class NavigationBar extends ImmutablePureComponent {
   render () {
     return (
       <div className='navigation-bar'>
-        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
+        <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
           <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
           <Avatar account={this.props.account} size={48} />
         </Permalink>
 
         <div className='navigation-bar__profile'>
-          <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
+          <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
             <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
           </Permalink>
 
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index a1d5c420cb6..863defb768f 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -32,7 +32,7 @@ class ReplyIndicator extends ImmutablePureComponent {
   handleAccountClick = (e) => {
     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
+      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
   }
 
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index e2de8b0e6a2..2ca14a2dd2f 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -99,16 +99,16 @@ class Compose extends React.PureComponent {
         <nav className='drawer__header'>
           <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
           {!columns.some(column => column.get('id') === 'HOME') && (
-            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
+            <Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
           )}
           {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
             <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
           )}
           {!columns.some(column => column.get('id') === 'COMMUNITY') && (
-            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
+            <Link to='/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
           )}
           {!columns.some(column => column.get('id') === 'PUBLIC') && (
-            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
+            <Link to='/federated' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
           )}
           <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
           <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index 43e1d77b946..c4f7098c5db 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -133,7 +133,7 @@ class Conversation extends ImmutablePureComponent {
 
     menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
 
-    const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
+    const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
 
     const handlers = {
       reply: this.handleReply,
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
index 8f0e8db4b41..03e13f28e44 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.js
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -213,7 +213,7 @@ class AccountCard extends ImmutablePureComponent {
           <Permalink
             className='directory__card__bar__name'
             href={account.get('url')}
-            to={`/accounts/${account.get('id')}`}
+            to={`/@${account.get('acct')}`}
           >
             <Avatar account={account} size={48} />
             <DisplayName account={account} />
diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
index 8269f5ae43f..263a7ae1626 100644
--- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
+++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
@@ -30,7 +30,7 @@ class AccountAuthorize extends ImmutablePureComponent {
     return (
       <div className='account-authorize__wrapper'>
         <div className='account-authorize'>
-          <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'>
+          <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
             <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
             <DisplayName account={account} />
           </Permalink>
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index ae00d13d3b2..533e838419e 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { debounce } from 'lodash';
 import LoadingIndicator from '../../components/loading_indicator';
 import {
-  fetchAccount,
+  lookupAccount,
   fetchFollowers,
   expandFollowers,
 } from '../../actions/accounts';
@@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list';
 import MissingIndicator from 'mastodon/components/missing_indicator';
 import TimelineHint from 'mastodon/components/timeline_hint';
 
-const mapStateToProps = (state, props) => ({
-  remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
-  remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
-  isAccount: !!state.getIn(['accounts', props.params.accountId]),
-  accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
-  hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
-  isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true),
-  blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
-});
+const mapStateToProps = (state, { params: { acct } }) => {
+  const accountId = state.getIn(['accounts_map', acct]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
+    remoteUrl: state.getIn(['accounts', accountId, 'url']),
+    isAccount: !!state.getIn(['accounts', accountId]),
+    accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
+    hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
+    isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
+    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
+  };
+};
 
 const RemoteHint = ({ url }) => (
   <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
@@ -41,7 +52,10 @@ export default @connect(mapStateToProps)
 class Followers extends ImmutablePureComponent {
 
   static propTypes = {
-    params: PropTypes.object.isRequired,
+    params: PropTypes.shape({
+      acct: PropTypes.string.isRequired,
+    }).isRequired,
+    accountId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
     accountIds: ImmutablePropTypes.list,
@@ -54,22 +68,34 @@ class Followers extends ImmutablePureComponent {
     multiColumn: PropTypes.bool,
   };
 
-  componentWillMount () {
-    if (!this.props.accountIds) {
-      this.props.dispatch(fetchAccount(this.props.params.accountId));
-      this.props.dispatch(fetchFollowers(this.props.params.accountId));
+  _load () {
+    const { accountId, dispatch } = this.props;
+
+    dispatch(fetchFollowers(accountId));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
     }
   }
 
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(nextProps.params.accountId));
-      this.props.dispatch(fetchFollowers(nextProps.params.accountId));
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
     }
   }
 
   handleLoadMore = debounce(() => {
-    this.props.dispatch(expandFollowers(this.props.params.accountId));
+    this.props.dispatch(expandFollowers(this.props.accountId));
   }, 300, { leading: true });
 
   render () {
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index 666ec7a7f66..c302706d512 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { debounce } from 'lodash';
 import LoadingIndicator from '../../components/loading_indicator';
 import {
-  fetchAccount,
+  lookupAccount,
   fetchFollowing,
   expandFollowing,
 } from '../../actions/accounts';
@@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list';
 import MissingIndicator from 'mastodon/components/missing_indicator';
 import TimelineHint from 'mastodon/components/timeline_hint';
 
-const mapStateToProps = (state, props) => ({
-  remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
-  remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
-  isAccount: !!state.getIn(['accounts', props.params.accountId]),
-  accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
-  hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
-  isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true),
-  blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
-});
+const mapStateToProps = (state, { params: { acct } }) => {
+  const accountId = state.getIn(['accounts_map', acct]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
+    remoteUrl: state.getIn(['accounts', accountId, 'url']),
+    isAccount: !!state.getIn(['accounts', accountId]),
+    accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
+    hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
+    isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
+    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
+  };
+};
 
 const RemoteHint = ({ url }) => (
   <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
@@ -41,7 +52,10 @@ export default @connect(mapStateToProps)
 class Following extends ImmutablePureComponent {
 
   static propTypes = {
-    params: PropTypes.object.isRequired,
+    params: PropTypes.shape({
+      acct: PropTypes.string.isRequired,
+    }).isRequired,
+    accountId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
     accountIds: ImmutablePropTypes.list,
@@ -54,22 +68,34 @@ class Following extends ImmutablePureComponent {
     multiColumn: PropTypes.bool,
   };
 
-  componentWillMount () {
-    if (!this.props.accountIds) {
-      this.props.dispatch(fetchAccount(this.props.params.accountId));
-      this.props.dispatch(fetchFollowing(this.props.params.accountId));
+  _load () {
+    const { accountId, dispatch } = this.props;
+
+    dispatch(fetchFollowing(accountId));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
     }
   }
 
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(nextProps.params.accountId));
-      this.props.dispatch(fetchFollowing(nextProps.params.accountId));
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
     }
   }
 
   handleLoadMore = debounce(() => {
-    this.props.dispatch(expandFollowing(this.props.params.accountId));
+    this.props.dispatch(expandFollowing(this.props.accountId));
   }, 300, { leading: true });
 
   render () {
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js
index ff1566e05ca..24db8cedec6 100644
--- a/app/javascript/mastodon/features/getting_started/components/announcements.js
+++ b/app/javascript/mastodon/features/getting_started/components/announcements.js
@@ -87,7 +87,7 @@ class Content extends ImmutablePureComponent {
   onMentionClick = (mention, e) => {
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${mention.get('id')}`);
+      this.context.router.history.push(`/@${mention.get('acct')}`);
     }
   }
 
@@ -96,14 +96,14 @@ class Content extends ImmutablePureComponent {
 
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/timelines/tag/${hashtag}`);
+      this.context.router.history.push(`/tags/${hashtag}`);
     }
   }
 
   onStatusClick = (status, e) => {
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/statuses/${status.get('id')}`);
+      this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
     }
   }
 
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 1b999461290..88762d60764 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent {
     const { fetchFollowRequests, multiColumn } = this.props;
 
     if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
-      this.context.router.history.replace('/timelines/home');
+      this.context.router.history.replace('/home');
       return;
     }
 
@@ -98,15 +98,15 @@ class GettingStarted extends ImmutablePureComponent {
     if (multiColumn) {
       navItems.push(
         <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
-        <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
-        <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
+        <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />,
+        <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />,
       );
 
       height += 34 + 48*2;
 
       if (profile_directory) {
         navItems.push(
-          <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
+          <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/explore' />,
         );
 
         height += 48;
@@ -119,7 +119,7 @@ class GettingStarted extends ImmutablePureComponent {
       height += 34;
     } else if (profile_directory) {
       navItems.push(
-        <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
+        <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/explore' />,
       );
 
       height += 48;
@@ -127,13 +127,13 @@ class GettingStarted extends ImmutablePureComponent {
 
     if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
       navItems.push(
-        <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />,
+        <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />,
       );
       height += 48;
     }
 
     navItems.push(
-      <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
+      <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />,
       <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
       <ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
       <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 577ff33bb03..0d7914f1f68 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -153,7 +153,7 @@ class HomeTimeline extends React.PureComponent {
           scrollKey={`home_timeline-${columnId}`}
           onLoadMore={this.handleLoadMore}
           timelineId='home'
-          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
+          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
           shouldUpdateScroll={shouldUpdateScroll}
           bindToDocument={!multiColumn}
         />
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
index ca1fa1f5ef0..b52116fc283 100644
--- a/app/javascript/mastodon/features/lists/index.js
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -74,7 +74,7 @@ class Lists extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {lists.map(list =>
-            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
+            <ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.js b/app/javascript/mastodon/features/notifications/components/follow_request.js
index a80cfb2fa1f..9ef3fde7eb0 100644
--- a/app/javascript/mastodon/features/notifications/components/follow_request.js
+++ b/app/javascript/mastodon/features/notifications/components/follow_request.js
@@ -42,7 +42,7 @@ class FollowRequest extends ImmutablePureComponent {
     return (
       <div className='account'>
         <div className='account__wrapper'>
-          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
+          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
             <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
             <DisplayName account={account} />
           </Permalink>
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 94fdbd6f453..f9f8a87f2db 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -68,7 +68,7 @@ class Notification extends ImmutablePureComponent {
     const { notification } = this.props;
 
     if (notification.get('status')) {
-      this.context.router.history.push(`/statuses/${notification.get('status')}`);
+      this.context.router.history.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`);
     } else {
       this.handleOpenProfile();
     }
@@ -76,7 +76,7 @@ class Notification extends ImmutablePureComponent {
 
   handleOpenProfile = () => {
     const { notification } = this.props;
-    this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
+    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
   }
 
   handleMention = e => {
@@ -315,7 +315,7 @@ class Notification extends ImmutablePureComponent {
     const { notification } = this.props;
     const account          = notification.get('account');
     const displayNameHtml  = { __html: account.get('display_name_html') };
-    const link             = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
+    const link             = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
 
     switch(notification.get('type')) {
     case 'follow':
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 1ecb18bf820..9f237da461d 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -116,7 +116,7 @@ class Footer extends ImmutablePureComponent {
 
     const { status } = this.props;
 
-    router.history.push(`/statuses/${status.get('id')}`);
+    router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
   }
 
   render () {
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.js b/app/javascript/mastodon/features/picture_in_picture/components/header.js
index 7dd199b7525..e05d8c62e90 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/header.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/header.js
@@ -34,7 +34,7 @@ class Header extends ImmutablePureComponent {
 
     return (
       <div className='picture-in-picture__header'>
-        <Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'>
+        <Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
           <Avatar account={account} size={36} />
           <DisplayName account={account} />
         </Link>
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 043a749ede2..72ddeb2b24d 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -55,7 +55,7 @@ class DetailedStatus extends ImmutablePureComponent {
   handleAccountClick = (e) => {
     if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
       e.preventDefault();
-      this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
+      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
 
     e.stopPropagation();
@@ -195,7 +195,7 @@ class DetailedStatus extends ImmutablePureComponent {
       reblogLink = (
         <React.Fragment>
           <React.Fragment> · </React.Fragment>
-          <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
+          <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
             <Icon id={reblogIcon} />
             <span className='detailed-status__reblogs'>
               <AnimatedNumber value={status.get('reblogs_count')} />
@@ -219,7 +219,7 @@ class DetailedStatus extends ImmutablePureComponent {
 
     if (this.context.router) {
       favouriteLink = (
-        <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
+        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
           <Icon id='star' />
           <span className='detailed-status__favorites'>
             <AnimatedNumber value={status.get('favourites_count')} />
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index df8362a1bc7..d1f63d4251c 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -396,7 +396,7 @@ class Status extends ImmutablePureComponent {
   }
 
   handleHotkeyOpenProfile = () => {
-    this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
+    this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
   }
 
   handleHotkeyToggleHidden = () => {
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js
index 83229833b15..f8a344690e3 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.js
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.js
@@ -68,7 +68,7 @@ class BoostModal extends ImmutablePureComponent {
     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
       this.props.onClose();
-      this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
+      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 6837450eb58..46b0fda8ad3 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -191,7 +191,7 @@ class ColumnsArea extends ImmutablePureComponent {
     const columnIndex = getIndex(this.context.router.history.location.pathname);
 
     if (singleColumn) {
-      const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
+      const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
 
       const content = columnIndex !== -1 ? (
         <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}>
diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js
index 3d0c48c7a96..82a21d515ae 100644
--- a/app/javascript/mastodon/features/ui/components/compose_panel.js
+++ b/app/javascript/mastodon/features/ui/components/compose_panel.js
@@ -6,6 +6,7 @@ import ComposeFormContainer from 'mastodon/features/compose/containers/compose_f
 import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
 import LinkFooter from './link_footer';
 import { changeComposing } from 'mastodon/actions/compose';
+import { me } from 'mastodon/initial_state';
 
 export default @connect()
 class ComposePanel extends React.PureComponent {
@@ -26,8 +27,10 @@ class ComposePanel extends React.PureComponent {
     return (
       <div className='compose-panel' onFocus={this.onFocus}>
         <SearchContainer openInRoute />
-        <NavigationContainer onClose={this.onBlur} />
-        <ComposeFormContainer singleColumn />
+
+        {me && <NavigationContainer onClose={this.onBlur} />}
+        {me && <ComposeFormContainer singleColumn />}
+
         <LinkFooter withHotkeys />
       </div>
     );
diff --git a/app/javascript/mastodon/features/ui/components/list_panel.js b/app/javascript/mastodon/features/ui/components/list_panel.js
index 1f7ec683a7f..411f62508ed 100644
--- a/app/javascript/mastodon/features/ui/components/list_panel.js
+++ b/app/javascript/mastodon/features/ui/components/list_panel.js
@@ -46,7 +46,7 @@ class ListPanel extends ImmutablePureComponent {
         <hr />
 
         {lists.map(list => (
-          <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
+          <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/lists/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
         ))}
       </div>
     );
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 08da1033042..dc21946645a 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -152,13 +152,6 @@ class MediaModal extends ImmutablePureComponent {
     }));
   };
 
-  handleStatusClick = e => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.context.router.history.push(`/statuses/${this.props.statusId}`);
-    }
-  }
-
   render () {
     const { media, statusId, intl, onClose } = this.props;
     const { navigationHidden } = this.state;
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 0c12852f5b1..26b2cfc8780 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -10,16 +10,16 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
 
 const NavigationPanel = () => (
   <div className='navigation-panel'>
-    <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
     <FollowRequestsNavLink />
-    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
-    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
-    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
+    <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
-    {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
+    {profile_directory && <NavLink className='column-link column-link--transparent' to='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
 
     <ListPanel />
 
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index 1911da8ba3f..a023bcf3412 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -8,10 +8,10 @@ import Icon from 'mastodon/components/icon';
 import NotificationsCounterIcon from './notifications_counter_icon';
 
 export const links = [
-  <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
-  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
-  <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
+  <NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
   <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
   <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
 ];
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 507ac1df146..c0b7bafce66 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -147,7 +147,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 
   render () {
     const { children, mobile } = this.props;
-    const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
+    const redirect = mobile ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
 
     return (
       <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
@@ -155,31 +155,29 @@ class SwitchingColumnsArea extends React.PureComponent {
           {redirect}
           <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
-          <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-
+          <WrappedRoute path='/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/conversations' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
           <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-
           <WrappedRoute path='/search' component={Search} content={children} />
-          <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-
-          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
-          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-
-          <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }} />
-          <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/explore' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/publish' component={Compose} content={children} />
+
+          <WrappedRoute path='/@:acct' exact component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/with_replies' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }} />
+          <WrappedRoute path='/@:acct/followers' component={Followers} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/following' component={Following} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/media' component={AccountGallery} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/tagged/:tag' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 
           <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/blocks' component={Blocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
@@ -447,7 +445,7 @@ class UI extends React.PureComponent {
   }
 
   handleHotkeyGoToHome = () => {
-    this.context.router.history.push('/timelines/home');
+    this.context.router.history.push('/home');
   }
 
   handleHotkeyGoToNotifications = () => {
@@ -455,15 +453,15 @@ class UI extends React.PureComponent {
   }
 
   handleHotkeyGoToLocal = () => {
-    this.context.router.history.push('/timelines/public/local');
+    this.context.router.history.push('/public/local');
   }
 
   handleHotkeyGoToFederated = () => {
-    this.context.router.history.push('/timelines/public');
+    this.context.router.history.push('/public');
   }
 
   handleHotkeyGoToDirect = () => {
-    this.context.router.history.push('/timelines/direct');
+    this.context.router.history.push('/conversations');
   }
 
   handleHotkeyGoToStart = () => {
@@ -479,7 +477,7 @@ class UI extends React.PureComponent {
   }
 
   handleHotkeyGoToProfile = () => {
-    this.context.router.history.push(`/accounts/${me}`);
+    this.context.router.history.push(`/accounts/${me}`); // FIXME
   }
 
   handleHotkeyGoToBlocked = () => {
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index bda51f692b7..91d36405bc1 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -13,9 +13,9 @@ function main() {
   if (window.history && history.replaceState) {
     const { pathname, search, hash } = window.location;
     const path = pathname + search + hash;
-    if (!(/^\/web($|\/)/).test(path)) {
-      history.replaceState(null, document.title, `/web${path}`);
-    }
+    //if (!(/^\/web($|\/)/).test(path)) {
+    //  history.replaceState(null, document.title, `/web${path}`);
+    //}
   }
 
   ready(() => {
diff --git a/app/javascript/mastodon/reducers/accounts_map.js b/app/javascript/mastodon/reducers/accounts_map.js
new file mode 100644
index 00000000000..e0d42e9cd44
--- /dev/null
+++ b/app/javascript/mastodon/reducers/accounts_map.js
@@ -0,0 +1,15 @@
+import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
+import { Map as ImmutableMap } from 'immutable';
+
+const initialState = ImmutableMap();
+
+export default function accountsMap(state = initialState, action) {
+  switch(action.type) {
+  case ACCOUNT_IMPORT:
+    return state.set(action.account.acct, action.account.id);
+  case ACCOUNTS_IMPORT:
+    return state.withMutations(map => action.accounts.forEach(account => map.set(account.acct, account.id)));
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 3b3c5ae2995..e518c8228ab 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -38,6 +38,7 @@ import missed_updates from './missed_updates';
 import announcements from './announcements';
 import markers from './markers';
 import picture_in_picture from './picture_in_picture';
+import accounts_map from './accounts_map';
 
 const reducers = {
   announcements,
@@ -52,6 +53,7 @@ const reducers = {
   status_lists,
   accounts,
   accounts_counters,
+  accounts_map,
   statuses,
   relationships,
   settings,
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index 958e5fc12c8..2da78006b8e 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -90,7 +90,7 @@ const handlePush = (event) => {
       options.tag       = notification.id;
       options.badge     = '/badge.png';
       options.image     = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined;
-      options.data      = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` };
+      options.data      = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/${notification.status.id}` : `/@${notification.account.acct}` };
 
       if (notification.status && notification.status.spoiler_text || notification.status.sensitive) {
         options.data.hiddenBody  = htmlToPlainText(notification.status.content);
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 1a81b96f6c9..a64cdfa2c16 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -16,71 +16,13 @@
   = opengraph 'og:type', 'profile'
   = render 'og', account: @account, url: short_account_url(@account, only_path: false)
 
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-= render 'header', account: @account, with_bio: true
+.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
-.grid
-  .column-0
-    .h-feed
-      %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
-
-      .account__section-headline
-        = active_link_to t('accounts.posts_tab_heading'), short_account_url(@account)
-        = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
-        = active_link_to t('accounts.media'), short_account_media_url(@account)
-
-      - if user_signed_in? && @account.blocking?(current_account)
-        .nothing-here.nothing-here--under-tabs= t('accounts.unavailable')
-      - elsif @statuses.empty?
-        = nothing_here 'nothing-here--under-tabs'
-      - else
-        .activity-stream.activity-stream--under-tabs
-          - if params[:page].to_i.zero?
-            = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
-
-          - if @newer_url
-            .entry= link_to_newer @newer_url
-
-          = render partial: 'statuses/status', collection: @statuses, as: :status
-
-          - if @older_url
-            .entry= link_to_older @older_url
-
-  .column-1
-    - if @account.memorial?
-      .memoriam-widget= t('in_memoriam_html')
-    - elsif @account.moved?
-      = render 'moved', account: @account
-
-    = render 'bio', account: @account
-
-    - if @endorsed_accounts.empty? && @account.id == current_account&.id
-      .placeholder-widget= t('accounts.endorsements_hint')
-    - elsif !@endorsed_accounts.empty?
-      .endorsements-widget
-        %h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
-
-        - @endorsed_accounts.each do |account|
-          = account_link_to account
-
-    - if @featured_hashtags.empty? && @account.id == current_account&.id
-      .placeholder-widget
-        = t('accounts.featured_tags_hint')
-        = link_to settings_featured_tags_path do
-          = t('featured_tags.add_new')
-          = fa_icon 'chevron-right fw'
-    - else
-      - @featured_hashtags.each do |featured_tag|
-        .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
-          = link_to short_account_tag_path(@account, featured_tag.tag) do
-            %h4
-              = fa_icon 'hashtag'
-              = featured_tag.name
-              %small
-                - if featured_tag.last_status_at.nil?
-                  = t('accounts.nothing_here')
-                - else
-                  %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
-            .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
-
-    = render 'application/sidebar'
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/authorize_interactions/_post_follow_actions.html.haml b/app/views/authorize_interactions/_post_follow_actions.html.haml
index dd71160e2d7..611e4a9c053 100644
--- a/app/views/authorize_interactions/_post_follow_actions.html.haml
+++ b/app/views/authorize_interactions/_post_follow_actions.html.haml
@@ -1,4 +1,4 @@
 .post-follow-actions
-  %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@resource.id}"), class: 'button button--block'
+  %div= link_to t('authorize_follow.post_follow.web'), short_account_path(@resource.acct), class: 'button button--block'
   %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@resource), class: 'button button--block'
   %div= t('authorize_follow.post_follow.close')
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index 7975ee9997b..ec0f00a60ed 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -10,46 +10,13 @@
   = opengraph 'og:description', t('directories.explanation')
   = opengraph 'og:image', File.join(root_url, 'android-chrome-192x192.png')
 
-.page-header
-  %h1= t('directories.explore_mastodon', title: site_title)
-  %p= t('directories.explanation')
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-- if @accounts.empty?
-  = nothing_here
-- else
-  .directory__list
-    - @accounts.each do |account|
-      .directory__card
-        .directory__card__img
-          = image_tag account.header.url, alt: ''
-        .directory__card__bar
-          = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
-            .avatar
-              = image_tag account.avatar.url, alt: '', class: 'u-photo'
+.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
-            .display-name
-              %bdi
-                %strong.emojify.p-name= display_name(account, custom_emojify: true)
-              %span= acct(account)
-          .directory__card__bar__relationship.account__relationship
-            = minimal_account_action_button(account)
-
-        .directory__card__extra
-          .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
-
-        .directory__card__extra
-          .accounts-table__count
-            = number_to_human account.statuses_count, strip_insignificant_zeros: true
-            %small= t('accounts.posts', count: account.statuses_count).downcase
-          .accounts-table__count
-            = number_to_human account.followers_count, strip_insignificant_zeros: true
-            %small= t('accounts.followers', count: account.followers_count).downcase
-          .accounts-table__count
-            - if account.last_status_at.present?
-              %time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at.to_date
-            - else
-              = t('accounts.never_active')
-
-            %small= t('accounts.last_active')
-
-  = paginate @accounts
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 645dd2de17c..8b0e52a98fc 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -5,16 +5,13 @@
   %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
-= render 'accounts/header', account: @account
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-- if @account.user_hides_network?
-  .nothing-here= t('accounts.network_hidden')
-- elsif user_signed_in? && @account.blocking?(current_account)
-  .nothing-here= t('accounts.unavailable')
-- elsif @follows.empty?
-  = nothing_here
-- else
-  .card-grid
-    = render partial: 'application/card', collection: @follows.map(&:account), as: :account
+.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
-  = paginate @follows
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 17fe790188c..9b10d219724 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -5,16 +5,13 @@
   %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
-= render 'accounts/header', account: @account
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-- if @account.user_hides_network?
-  .nothing-here= t('accounts.network_hidden')
-- elsif user_signed_in? && @account.blocking?(current_account)
-  .nothing-here= t('accounts.unavailable')
-- elsif @follows.empty?
-  = nothing_here
-- else
-  .card-grid
-    = render partial: 'application/card', collection: @follows.map(&:target_account), as: :account
+.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
-  = paginate @follows
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 9b7e1b65c63..4a7c390eb3e 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -42,4 +42,4 @@
                                         = link_to a.remote_url, a.remote_url
 
                               %p.status-footer
-                                = link_to l(status.created_at), web_url("statuses/#{status.id}")
+                                = link_to l(status.created_at), short_account_status_url(status.account.acct, status.id)
diff --git a/app/views/notification_mailer/_status.text.erb b/app/views/notification_mailer/_status.text.erb
index 8999a1f8ea5..6d55741ecb2 100644
--- a/app/views/notification_mailer/_status.text.erb
+++ b/app/views/notification_mailer/_status.text.erb
@@ -5,4 +5,4 @@
 <% end %>
 <%= raw Formatter.instance.plaintext(status) %>
 
-<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %>
+<%= raw t('application_mailer.view')%> <%= short_account_status_url(status.account.acct, status.id) %>
diff --git a/app/views/notification_mailer/digest.html.haml b/app/views/notification_mailer/digest.html.haml
index a94ace228d8..f46e176d7be 100644
--- a/app/views/notification_mailer/digest.html.haml
+++ b/app/views/notification_mailer/digest.html.haml
@@ -19,7 +19,7 @@
                                 %tbody
                                   %tr
                                     %td.button-primary
-                                      = link_to web_url do
+                                      = link_to home_url do
                                         %span= t 'notification_mailer.digest.action'
 
 - @notifications.each_with_index do |n, i|
diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb
index b2c85a9e3dc..9dd81f64f99 100644
--- a/app/views/notification_mailer/digest.text.erb
+++ b/app/views/notification_mailer/digest.text.erb
@@ -7,7 +7,7 @@
 
   <%= raw Formatter.instance.plaintext(notification.target_status) %>
 
-  <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %>
+  <%= raw t('application_mailer.view')%> <%= short_account_status_url(notification.target_status.account.acct, notification.target_status.id) %>
 <% end %>
 <% if @follows_since > 0 %>
 
diff --git a/app/views/notification_mailer/favourite.html.haml b/app/views/notification_mailer/favourite.html.haml
index a715d615ce8..e59770fe371 100644
--- a/app/views/notification_mailer/favourite.html.haml
+++ b/app/views/notification_mailer/favourite.html.haml
@@ -41,5 +41,5 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to web_url("statuses/#{@status.id}") do
+                                  = link_to short_account_status_url(status.account.acct, status.id) do
                                     %span= t 'application_mailer.view_status'
diff --git a/app/views/notification_mailer/follow.html.haml b/app/views/notification_mailer/follow.html.haml
index cd84f785847..e9b3c8f96fe 100644
--- a/app/views/notification_mailer/follow.html.haml
+++ b/app/views/notification_mailer/follow.html.haml
@@ -39,5 +39,5 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to web_url("accounts/#{@account.id}") do
+                                  = link_to short_account_url(@account.acct) do
                                     %span= t 'application_mailer.view_profile'
diff --git a/app/views/notification_mailer/follow.text.erb b/app/views/notification_mailer/follow.text.erb
index cbe46f55241..a5e6019e7e0 100644
--- a/app/views/notification_mailer/follow.text.erb
+++ b/app/views/notification_mailer/follow.text.erb
@@ -2,4 +2,4 @@
 
 <%= raw t('notification_mailer.follow.body', name: @account.acct) %>
 
-<%= raw t('application_mailer.view')%> <%= web_url("accounts/#{@account.id}") %>
+<%= raw t('application_mailer.view')%> <%= short_account_url(@account.acct) %>
diff --git a/app/views/notification_mailer/follow_request.html.haml b/app/views/notification_mailer/follow_request.html.haml
index a63e27a909e..f2d04b9ebe6 100644
--- a/app/views/notification_mailer/follow_request.html.haml
+++ b/app/views/notification_mailer/follow_request.html.haml
@@ -39,5 +39,5 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to web_url("follow_requests") do
+                                  = link_to follow_requests_url do
                                     %span= t 'notification_mailer.follow_request.action'
diff --git a/app/views/notification_mailer/follow_request.text.erb b/app/views/notification_mailer/follow_request.text.erb
index a018394b857..e2a608e98f9 100644
--- a/app/views/notification_mailer/follow_request.text.erb
+++ b/app/views/notification_mailer/follow_request.text.erb
@@ -2,4 +2,4 @@
 
 <%= raw t('notification_mailer.follow_request.body', name: @account.acct) %>
 
-<%= raw t('application_mailer.view')%> <%= web_url("follow_requests") %>
+<%= raw t('application_mailer.view')%> <%= follow_requests_url %>
diff --git a/app/views/notification_mailer/mention.html.haml b/app/views/notification_mailer/mention.html.haml
index 619873cfa3a..34bfcc7044c 100644
--- a/app/views/notification_mailer/mention.html.haml
+++ b/app/views/notification_mailer/mention.html.haml
@@ -41,5 +41,5 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to web_url("statuses/#{@status.id}") do
+                                  = link_to short_account_status_url(@status.account.acct, @status.id) do
                                     %span= t 'notification_mailer.mention.action'
diff --git a/app/views/notification_mailer/reblog.html.haml b/app/views/notification_mailer/reblog.html.haml
index a2811be2326..16845c27920 100644
--- a/app/views/notification_mailer/reblog.html.haml
+++ b/app/views/notification_mailer/reblog.html.haml
@@ -41,5 +41,5 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to web_url("statuses/#{@status.id}") do
+                                  = link_to short_account_status_url(@status.account.acct, @status.id) do
                                     %span= t 'application_mailer.view_status'
diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml
index 9254bd34852..5c208f9066b 100644
--- a/app/views/public_timelines/show.html.haml
+++ b/app/views/public_timelines/show.html.haml
@@ -3,15 +3,13 @@
 
 - content_for :header_tags do
   %meta{ name: 'robots', content: 'noindex' }/
-  = javascript_pack_tag 'about', crossorigin: 'anonymous'
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-.page-header
-  %h1= t('about.see_whats_happening')
+.app-holder.notranslate#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
-  - if Setting.show_known_fediverse_at_about_page
-    %p= t('about.browse_public_posts')
-  - else
-    %p= t('about.browse_local_posts')
-
-#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(local: !Setting.show_known_fediverse_at_about_page)) }}
-.notranslate#modal-container
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 93af131e5a6..a0077701c88 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -77,4 +77,4 @@
 
     - if user_signed_in?
       ·
-      = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank'
+      = link_to t('statuses.open_in_web'), short_account_status_url(status.account.acct, status.id), class: 'detailed-status__application', target: '_blank'
diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml
index 7ef7b09a2de..0e2ef755dfc 100644
--- a/app/views/statuses/show.html.haml
+++ b/app/views/statuses/show.html.haml
@@ -17,9 +17,13 @@
   = render 'og_description', activity: @status
   = render 'og_image', activity: @status, account: @account
 
-.grid
-  .column-0
-    .activity-stream.h-entry
-      = render partial: 'status', locals: { status: @status, include_threads: true }
-  .column-1
-    = render 'application/sidebar'
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
+
+.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
+
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 5cd513b320e..85032af98e0 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -5,12 +5,15 @@
   %meta{ name: 'robots', content: 'noindex' }/
   %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
 
-  = javascript_pack_tag 'about', crossorigin: 'anonymous'
   = render 'og'
 
-.page-header
-  %h1= "##{@tag.name}"
-  %p= t('about.about_hashtag_html', hashtag: @tag.name)
+  %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
+  = render_initial_state
+  = javascript_pack_tag 'application', crossorigin: 'anonymous'
 
-#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name, local: @local)) }}
-.notranslate#modal-container
+.app-holder.notranslate#mastodon{ data: { props: Oj.dump(default_props) } }
+  %noscript
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
+
+    %div
+      = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/user_mailer/welcome.html.haml b/app/views/user_mailer/welcome.html.haml
index 1f75ff48ae4..d9fd1cd4fb2 100644
--- a/app/views/user_mailer/welcome.html.haml
+++ b/app/views/user_mailer/welcome.html.haml
@@ -114,7 +114,7 @@
                                 %tbody
                                   %tr
                                     %td.button-primary
-                                      = link_to web_url do
+                                      = link_to home_url do
                                         %span= t 'user_mailer.welcome.final_action'
 
 %table.email-table{ cellspacing: 0, cellpadding: 0 }
diff --git a/app/views/user_mailer/welcome.text.erb b/app/views/user_mailer/welcome.text.erb
index e310d7ca6f9..e97f80128bb 100644
--- a/app/views/user_mailer/welcome.text.erb
+++ b/app/views/user_mailer/welcome.text.erb
@@ -17,7 +17,7 @@
 
 <%= t 'user_mailer.welcome.final_step' %>
 
-=> <%= web_url %>
+=> <%= home_url %>
 
 ---
 
diff --git a/config/routes.rb b/config/routes.rb
index a534b433e07..c0b385cd73c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -5,6 +5,25 @@ require 'sidekiq-scheduler/web'
 
 Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
 
+# These paths do not have special server-side controllers and must load the web app only
+APP_PATHS = %w(
+  /home
+  /getting-started
+  /keyboard-shortcuts
+  /conversations
+  /lists/(*any)
+  /notifications
+  /favourites
+  /bookmarks
+  /pinned
+  /search
+  /publish
+  /follow_requests
+  /blocks
+  /domain_blocks
+  /mutes
+).freeze
+
 Rails.application.routes.draw do
   root 'home#index'
 
@@ -76,8 +95,6 @@ Rails.application.routes.draw do
 
     resources :followers, only: [:index], controller: :follower_accounts
     resources :following, only: [:index], controller: :following_accounts
-    resource :follow, only: [:create], controller: :account_follow
-    resource :unfollow, only: [:create], controller: :account_unfollow
 
     resource :outbox, only: [:show], module: :activitypub
     resource :inbox, only: [:create], module: :activitypub
@@ -92,6 +109,8 @@ Rails.application.routes.draw do
   get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
   get '/@:username/media', to: 'accounts#show', as: :short_account_media
   get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
+  get '/@:username/followers', to: 'follower_accounts#index', as: :short_account_followers
+  get '/@:username/following', to: 'following_accounts#index', as: :short_account_following
   get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
   get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
 
@@ -99,7 +118,6 @@ Rails.application.routes.draw do
   post '/interact/:id', to: 'remote_interaction#create'
 
   get '/explore', to: 'directories#index', as: :explore
-  get '/explore/:id', to: 'directories#show', as: :explore_hashtag
 
   get '/settings', to: redirect('/settings/profile')
 
@@ -181,6 +199,7 @@ Rails.application.routes.draw do
   resource :relationships, only: [:show, :update]
 
   get '/public', to: 'public_timelines#show', as: :public_timeline
+  get '/public/local', to: 'public_timelines#show', as: :local_public_timeline
   get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
 
   resource :authorize_interaction, only: [:show, :create]
@@ -431,6 +450,7 @@ Rails.application.routes.draw do
         get :verify_credentials, to: 'credentials#show'
         patch :update_credentials, to: 'credentials#update'
         resource :search, only: :show, controller: :search
+        resource :lookup, only: :show, controller: :lookup
         resources :relationships, only: :index
       end
 
@@ -515,7 +535,9 @@ Rails.application.routes.draw do
     end
   end
 
-  get '/web/(*any)', to: 'home#index', as: :web
+  APP_PATHS.each do |app_path|
+    get app_path, to: 'home#index'
+  end
 
   get '/about',        to: 'about#show'
   get '/about/more',   to: 'about#more'
-- 
GitLab