diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png
deleted file mode 100644
index 9c564bb61411bf4f0dec3e21aa86848ac70cbcb8..0000000000000000000000000000000000000000
Binary files a/app/assets/images/brand_logo.png and /dev/null differ
diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c157546b9cf87d9aaf9a404c01f8a623f7c24c7
Binary files /dev/null and b/app/assets/images/gitlab_logo.png differ
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 9bf750124b25085361aafff6c982916ef10a2c27..791cb9e50bd013b738d5d59c22a17ea098a6816b 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -8,6 +8,80 @@ module PageLayoutHelper
     @page_title.join(" \u00b7 ")
   end
 
+  # Define or get a description for the current page
+  #
+  # description - String (default: nil)
+  #
+  # If this helper is called multiple times with an argument, only the last
+  # description will be returned when called without an argument. Descriptions
+  # have newlines replaced with spaces and all HTML tags are sanitized.
+  #
+  # Examples:
+  #
+  #   page_description # => "GitLab Community Edition"
+  #   page_description("Foo")
+  #   page_description # => "Foo"
+  #
+  #   page_description("<b>Bar</b>\nBaz")
+  #   page_description # => "Bar Baz"
+  #
+  # Returns an HTML-safe String.
+  def page_description(description = nil)
+    @page_description ||= page_description_default
+
+    if description.present?
+      @page_description = description.squish
+    else
+      sanitize(@page_description, tags: []).truncate_words(30)
+    end
+  end
+
+  # Default value for page_description when one hasn't been defined manually by
+  # a view
+  def page_description_default
+    if @project
+      @project.description || brand_title
+    else
+      brand_title
+    end
+  end
+
+  def page_image
+    default = image_url('gitlab_logo.png')
+
+    if @project
+      @project.avatar_url || default
+    elsif @user
+      avatar_icon(@user)
+    else
+      default
+    end
+  end
+
+  # Define or get attributes to be used as Twitter card metadata
+  #
+  # map - Hash of label => data pairs. Keys become labels, values become data
+  #
+  # Raises ArgumentError if given more than two attributes
+  def page_card_attributes(map = {})
+    raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
+
+    @page_card_attributes ||= {}
+    @page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
+    @page_card_attributes
+  end
+
+  def page_card_meta_tags
+    tags = ''
+
+    page_card_attributes.each_with_index do |pair, i|
+      tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
+      tags << tag(:meta, property: "twitter:data#{i + 1}",  content: pair[1])
+    end
+
+    tags.html_safe
+  end
+
   def header_title(title = nil, title_url = nil)
     if title
       @header_title     = title
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f56fd3e02d472a06d839c18f6d63d189b01e896c..919833f6df5c2807f675b08175178457487ade99 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -161,6 +161,14 @@ module Issuable
     self.class.to_s.underscore
   end
 
+  # Returns a Hash of attributes to be used for Twitter card metadata
+  def card_attributes
+    {
+      'Author'   => author.try(:name),
+      'Assignee' => assignee.try(:name)
+    }
+  end
+
   def notes_with_associations
     notes.includes(:author, :project)
   end
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 74174a72f5a8c39d972e339c02d61f528063e8bf..2e0bd2007a3bf16b49c19741ebd638b570568466 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,10 +1,26 @@
-- page_title "GitLab"
-%head
+%head{prefix: "og: http://ogp.me/ns#"}
   %meta{charset: "utf-8"}
   %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
-  %meta{content: "GitLab Community Edition", name: "description"}
   %meta{name: 'referrer', content: 'origin-when-cross-origin'}
 
+  %meta{name: "description", content: page_description}
+
+  -# Open Graph - http://ogp.me/
+  %meta{property: 'og:type',        content: "object"}
+  %meta{property: 'og:site_name',   content: "GitLab"}
+  %meta{property: 'og:title',       content: page_title}
+  %meta{property: 'og:description', content: page_description}
+  %meta{property: 'og:image',       content: page_image}
+  %meta{property: 'og:url',         content: request.base_url + request.fullpath}
+
+  -# Twitter Card - https://dev.twitter.com/cards/types/summary
+  %meta{property: 'twitter:card',         content: "summary"}
+  %meta{property: 'twitter:title',        content: page_title}
+  %meta{property: 'twitter:description',  content: page_description}
+  %meta{property: 'twitter:image',        content: page_image}
+  = page_card_meta_tags
+
+  - page_title "GitLab"
   %title= page_title
 
   = favicon_link_tag 'favicon.ico'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b6efa05a1ae38b10727bd518e3ad164f2e4f4fab..f548383008daf828f4f5baf13d95d59019cb3777 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,7 @@
-- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title           "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_description     @issue.description
+- page_card_attributes @issue.card_attributes
+
 = render "header_title"
 
 .issue
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index e9ffbd06be26d94221df86d8ead8618d7f7770f0..ba7c2c01e93a7ac6760633a325672ab9db40b1e8 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,7 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title           "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_description     @merge_request.description
+- page_card_attributes @merge_request.card_attributes
+
 = render "header_title"
 
 - if params[:view] == 'parallel'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 7e73ae274e9c1bdf5c1258c4751f6a5ec2c9a1b9..1670ea8741a3a07065f93f38a89e4dc13b771812 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,4 +1,6 @@
-- page_title @milestone.title, "Milestones"
+- page_title       @milestone.title, "Milestones"
+- page_description @milestone.description
+
 = render "header_title"
 
 .detail-page-header
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index b7a7eb4e6f758e5845c2dcb218a1b3476e63b9b5..0bca8177e14e43f7fd892579a99612adbe19df58 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,5 +1,6 @@
-- page_title    @user.name
-- header_title  @user.name, user_path(@user)
+- page_title       @user.name
+- page_description @user.bio
+- header_title     @user.name, user_path(@user)
 
 = content_for :meta_tags do
   = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd7107779f6cd99b2d5f3349be12573025811062
--- /dev/null
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -0,0 +1,129 @@
+require 'rails_helper'
+
+describe PageLayoutHelper do
+  describe 'page_description' do
+    it 'defaults to value returned by page_description_default helper' do
+      allow(helper).to receive(:page_description_default).and_return('Foo')
+
+      expect(helper.page_description).to eq 'Foo'
+    end
+
+    it 'returns the last-pushed description' do
+      helper.page_description('Foo')
+      helper.page_description('Bar')
+      helper.page_description('Baz')
+
+      expect(helper.page_description).to eq 'Baz'
+    end
+
+    it 'squishes multiple newlines' do
+      helper.page_description("Foo\nBar\nBaz")
+
+      expect(helper.page_description).to eq 'Foo Bar Baz'
+    end
+
+    it 'truncates' do
+      helper.page_description <<-LOREM.strip_heredoc
+        Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo
+        ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis
+        dis parturient montes, nascetur ridiculus mus. Donec quam felis,
+        ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa
+        quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget,
+        arcu.
+      LOREM
+
+      expect(helper.page_description).to end_with 'quam felis,...'
+    end
+
+    it 'sanitizes all HTML' do
+      helper.page_description("<b>Bold</b> <h1>Header</h1>")
+
+      expect(helper.page_description).to eq 'Bold Header'
+    end
+  end
+
+  describe 'page_description_default' do
+    it 'uses Project description when available' do
+      project = double(description: 'Project Description')
+      helper.instance_variable_set(:@project, project)
+
+      expect(helper.page_description_default).to eq 'Project Description'
+    end
+
+    it 'uses brand_title when Project description is nil' do
+      project = double(description: nil)
+      helper.instance_variable_set(:@project, project)
+
+      expect(helper).to receive(:brand_title).and_return('Brand Title')
+      expect(helper.page_description_default).to eq 'Brand Title'
+    end
+
+    it 'falls back to brand_title' do
+      allow(helper).to receive(:brand_title).and_return('Brand Title')
+
+      expect(helper.page_description_default).to eq 'Brand Title'
+    end
+  end
+
+  describe 'page_image' do
+    it 'defaults to the GitLab logo' do
+      expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+    end
+
+    context 'with @project' do
+      it 'uses Project avatar if available' do
+        project = double(avatar_url: 'http://example.com/uploads/avatar.png')
+        helper.instance_variable_set(:@project, project)
+
+        expect(helper.page_image).to eq project.avatar_url
+      end
+
+      it 'falls back to the default' do
+        project = double(avatar_url: nil)
+        helper.instance_variable_set(:@project, project)
+
+        expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+      end
+    end
+
+    context 'with @user' do
+      it 'delegates to avatar_icon helper' do
+        user = double('User')
+        helper.instance_variable_set(:@user, user)
+
+        expect(helper).to receive(:avatar_icon).with(user)
+
+        helper.page_image
+      end
+    end
+  end
+
+  describe 'page_card_attributes' do
+    it 'raises ArgumentError when given more than two attributes' do
+      map = { foo: 'foo', bar: 'bar', baz: 'baz' }
+
+      expect { helper.page_card_attributes(map) }.
+        to raise_error(ArgumentError, /more than two attributes/)
+    end
+
+    it 'rejects blank values' do
+      map = { foo: 'foo', bar: '' }
+      helper.page_card_attributes(map)
+
+      expect(helper.page_card_attributes).to eq({ foo: 'foo' })
+    end
+  end
+
+  describe 'page_card_meta_tags' do
+    it 'returns the twitter:label and twitter:data tags' do
+      allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar')
+
+      tags = helper.page_card_meta_tags
+
+      aggregate_failures do
+        expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
+        expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
+      end
+    end
+  end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0f13c4410cdc482943c7d9e17c25b804e81acb57..f9d3c56750f200b0e4d7a1b4929d59e0283044b8 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -81,4 +81,22 @@ describe Issue, "Issuable" do
       expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
     end
   end
+
+  describe '#card_attributes' do
+    it 'includes the author name' do
+      allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+      allow(issue).to receive(:assignee).and_return(nil)
+
+      expect(issue.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+    end
+
+    it 'includes the assignee name' do
+      allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+      allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+      expect(issue.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+    end
+  end
 end