diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4e2d3bebb2e79c385c83e80fb57af7439b7002d3..fbf8b01b7c2a94ed0038909d87e2a92bac350da6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -62,8 +62,12 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def show
+    raw_notes = @issue.notes_with_associations.fresh
+
+    @notes = Banzai::NoteRenderer.
+      render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
+
     @note     = @project.notes.new(noteable: @issue)
-    @notes    = @issue.notes.with_associations.fresh
     @noteable = @issue
 
     respond_to do |format|
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 089669841d30d1088c78df86e70e38d330a878f6..79ba032996c2bc96dc872cbf03b5f9279c035260 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -85,6 +85,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
     @grouped_diff_notes = @merge_request.notes.grouped_diff_notes
 
+    Banzai::NoteRenderer.render(
+      @grouped_diff_notes.values.flatten,
+      @project,
+      current_user,
+      @path,
+      @project_wiki,
+      @ref
+    )
+
     respond_to do |format|
       format.html
       format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
@@ -325,8 +334,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   def define_show_vars
     # Build a note object for comment form
     @note = @project.notes.new(noteable: @merge_request)
-    @discussions = @merge_request.mr_and_commit_notes.inc_author_project_award_emoji.fresh.discussions
-    @notes = @discussions.flatten
+
+    @discussions = @merge_request.mr_and_commit_notes.
+      inc_author_project_award_emoji.
+      fresh.
+      discussions
+
+    @notes = Banzai::NoteRenderer.render(
+      @discussions.flatten,
+      @project,
+      current_user,
+      @path,
+      @project_wiki,
+      @ref
+    )
+
     @noteable = @merge_request
 
     # Get commits from repository
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 836f79ff0803a936c7d7cc93f02cb382ceda95b8..e14fe26dde7812af60cd2f1d6eb682b67a0be009 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -24,6 +24,10 @@ class Projects::NotesController < Projects::ApplicationController
   def create
     @note = Notes::CreateService.new(project, current_user, note_params).execute
 
+    if @note.is_a?(Note)
+      Banzai::NoteRenderer.render([@note], @project, current_user)
+    end
+
     respond_to do |format|
       format.json { render json: note_json(@note) }
       format.html { redirect_back_or_default }
@@ -33,6 +37,10 @@ class Projects::NotesController < Projects::ApplicationController
   def update
     @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
 
+    if @note.is_a?(Note)
+      Banzai::NoteRenderer.render([@note], @project, current_user)
+    end
+
     respond_to do |format|
       format.json { render json: note_json(@note) }
       format.html { redirect_back_or_default }
@@ -118,6 +126,8 @@ class Projects::NotesController < Projects::ApplicationController
         name:   note.name
       }
     elsif note.valid?
+      Banzai::NoteRenderer.render([note], @project, current_user)
+
       {
         valid: true,
         id: note.id,
diff --git a/app/models/note.rb b/app/models/note.rb
index e510525b89d2acfd49ef2ea55e7a51728f2e2232..8db500a521997532333cf5843870dfcc84c81092 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -6,6 +6,10 @@ class Note < ActiveRecord::Base
   include Awardable
   include Importable
 
+  # Attribute containing rendered and redacted Markdown as generated by
+  # Banzai::ObjectRenderer.
+  attr_accessor :note_html
+
   default_value_for :system, false
 
   attr_mentionable :note, pipeline: :note
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index c04d291412cf9bd4b9d42fc69aaf5bb2ed02129c..a5e163b91e90821d22b1738c1e16265988fdbf99 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -32,7 +32,7 @@
       .note-body{class: note_editable ? 'js-task-list-container' : ''}
         .note-text
           = preserve do
-            = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
+            = note.note_html
           = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
         - if note_editable
           = render 'projects/notes/edit_form', note: note
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index d159f4eded2dde2d6c8814e9030ab23784d33be9..75f89d524e71543b73c24883dec1b4ed9be14e74 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -113,6 +113,10 @@ if Gitlab::Metrics.enabled?
     config.instrument_methods(Banzai::Renderer)
     config.instrument_methods(Banzai::Querying)
 
+    config.instrument_instance_methods(Banzai::ObjectRenderer)
+    config.instrument_instance_methods(Banzai::Redactor)
+    config.instrument_methods(Banzai::NoteRenderer)
+
     [Issuable, Mentionable, Participable].each do |klass|
       config.instrument_instance_methods(klass)
       config.instrument_instance_methods(klass::ClassMethods)
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index c753a84a20d9a2d556cfc857d46f8f507d6ce2b4..c59a80dd1c7388441099cf9b14e6e05cf6cfdce6 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -7,40 +7,13 @@ module Banzai
     #
     class RedactorFilter < HTML::Pipeline::Filter
       def call
-        nodes = Querying.css(doc, 'a.gfm[data-reference-type]')
-        visible = nodes_visible_to_user(nodes)
-
-        nodes.each do |node|
-          unless visible.include?(node)
-            # The reference should be replaced by the original text,
-            # which is not always the same as the rendered text.
-            text = node.attr('data-original') || node.text
-            node.replace(text)
-          end
-        end
+        Redactor.new(project, current_user).redact([doc])
 
         doc
       end
 
       private
 
-      def nodes_visible_to_user(nodes)
-        per_type = Hash.new { |h, k| h[k] = [] }
-        visible = Set.new
-
-        nodes.each do |node|
-          per_type[node.attr('data-reference-type')] << node
-        end
-
-        per_type.each do |type, nodes|
-          parser = Banzai::ReferenceParser[type].new(project, current_user)
-
-          visible.merge(parser.nodes_visible_to_user(current_user, nodes))
-        end
-
-        visible
-      end
-
       def current_user
         context[:current_user]
       end
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bab6a9934d171ca4213f42e3db37ed79d7b8a0c7
--- /dev/null
+++ b/lib/banzai/note_renderer.rb
@@ -0,0 +1,22 @@
+module Banzai
+  module NoteRenderer
+    # Renders a collection of Note instances.
+    #
+    # notes - The notes to render.
+    # project - The project to use for rendering/redacting.
+    # user - The user viewing the notes.
+    # path - The request path.
+    # wiki - The project's wiki.
+    # git_ref - The current Git reference.
+    def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil)
+      renderer = ObjectRenderer.new(project,
+                                    user,
+                                    requested_path: path,
+                                    project_wiki: wiki,
+                                    ref: git_ref,
+                                    pipeline: :note)
+
+      renderer.render(notes, :note)
+    end
+  end
+end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0e4f28bf12312c67fa12472a5e9f7e8876a642b
--- /dev/null
+++ b/lib/banzai/object_renderer.rb
@@ -0,0 +1,85 @@
+module Banzai
+  # Class for rendering multiple objects (e.g. Note instances) in a single pass.
+  #
+  # Rendered Markdown is stored in an attribute in every object based on the
+  # name of the attribute containing the Markdown. For example, when the
+  # attribute `note` is rendered the HTML is stored in `note_html`.
+  class ObjectRenderer
+    attr_reader :project, :user
+
+    # Make sure to set the appropriate pipeline in the `raw_context` attribute
+    # (e.g. `:note` for Note instances).
+    #
+    # project - A Project to use for rendering and redacting Markdown.
+    # user - The user viewing the Markdown/HTML documents, if any.
+    # context - A Hash containing extra attributes to use in the rendering
+    #           pipeline.
+    def initialize(project, user = nil, raw_context = {})
+      @project = project
+      @user = user
+      @raw_context = raw_context
+    end
+
+    # Renders and redacts an Array of objects.
+    #
+    # objects - The objects to render
+    # attribute - The attribute containing the raw Markdown to render.
+    #
+    # Returns the same input objects.
+    def render(objects, attribute)
+      documents = render_objects(objects, attribute)
+      redacted = redact_documents(documents)
+
+      objects.each_with_index do |object, index|
+        object.__send__("#{attribute}_html=", redacted.fetch(index))
+      end
+
+      objects
+    end
+
+    # Renders the attribute of every given object.
+    def render_objects(objects, attribute)
+      objects.map do |object|
+        render_attribute(object, attribute)
+      end
+    end
+
+    # Redacts the list of documents.
+    #
+    # Returns an Array containing the redacted documents.
+    def redact_documents(documents)
+      redactor = Redactor.new(project, user)
+
+      redactor.redact(documents).map do |document|
+        document.to_html.html_safe
+      end
+    end
+
+    # Returns a Banzai context for the given object and attribute.
+    def context_for(object, attribute)
+      context = base_context.merge(cache_key: [object, attribute])
+
+      if object.respond_to?(:author)
+        context[:author] = object.author
+      end
+
+      context
+    end
+
+    # Renders the attribute of an object.
+    #
+    # Returns a `Nokogiri::HTML::Document`.
+    def render_attribute(object, attribute)
+      context = context_for(object, attribute)
+
+      string = object.__send__(attribute)
+      html = Banzai.render(string, context)
+
+      Banzai::Pipeline[:relative_link].to_document(html, context)
+    end
+
+    def base_context
+      @base_context ||= @raw_context.merge(current_user: user, project: project)
+    end
+  end
+end
diff --git a/lib/banzai/pipeline/relative_link_pipeline.rb b/lib/banzai/pipeline/relative_link_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..270990e7ab4ee5666ccf95b7361b18f54a392414
--- /dev/null
+++ b/lib/banzai/pipeline/relative_link_pipeline.rb
@@ -0,0 +1,11 @@
+module Banzai
+  module Pipeline
+    class RelativeLinkPipeline < BasePipeline
+      def self.filters
+        FilterArray[
+          Filter::RelativeLinkFilter
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ffd267d5e9ae48421f8868c1099d73fc0d670363
--- /dev/null
+++ b/lib/banzai/redactor.rb
@@ -0,0 +1,69 @@
+module Banzai
+  # Class for removing Markdown references a certain user is not allowed to
+  # view.
+  class Redactor
+    attr_reader :user, :project
+
+    # project - A Project to use for redacting links.
+    # user - The currently logged in user (if any).
+    def initialize(project, user = nil)
+      @project = project
+      @user = user
+    end
+
+    # Redacts the references in the given Array of documents.
+    #
+    # This method modifies the given documents in-place.
+    #
+    # documents - A list of HTML documents containing references to redact.
+    #
+    # Returns the documents passed as the first argument.
+    def redact(documents)
+      nodes = documents.flat_map do |document|
+        Querying.css(document, 'a.gfm[data-reference-type]')
+      end
+
+      redact_nodes(nodes)
+
+      documents
+    end
+
+    # Redacts the given nodes
+    #
+    # nodes - An Array of HTML nodes to redact.
+    def redact_nodes(nodes)
+      visible = nodes_visible_to_user(nodes)
+
+      nodes.each do |node|
+        unless visible.include?(node)
+          # The reference should be replaced by the original text,
+          # which is not always the same as the rendered text.
+          text = node.attr('data-original') || node.text
+          node.replace(text)
+        end
+      end
+    end
+
+    # Returns the nodes visible to the current user.
+    #
+    # nodes - The input nodes to check.
+    #
+    # Returns a new Array containing the visible nodes.
+    def nodes_visible_to_user(nodes)
+      per_type = Hash.new { |h, k| h[k] = [] }
+      visible = Set.new
+
+      nodes.each do |node|
+        per_type[node.attr('data-reference-type')] << node
+      end
+
+      per_type.each do |type, nodes|
+        parser = Banzai::ReferenceParser[type].new(project, user)
+
+        visible.merge(parser.nodes_visible_to_user(user, nodes))
+      end
+
+      visible
+    end
+  end
+end
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..98f76f36fd50349fe383e1142e7104912c265a1c
--- /dev/null
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Banzai::NoteRenderer do
+  describe '.render' do
+    it 'renders a Note' do
+      note = double(:note)
+      project = double(:project)
+      wiki = double(:wiki)
+      user = double(:user)
+
+      expect(Banzai::ObjectRenderer).to receive(:new).
+        with(project, user,
+             requested_path: 'foo',
+             project_wiki: wiki,
+             ref: 'bar',
+             pipeline: :note).
+        and_call_original
+
+      expect_any_instance_of(Banzai::ObjectRenderer).
+        to receive(:render).with([note], :note)
+
+      described_class.render([note], project, user, 'foo', wiki, 'bar')
+    end
+  end
+end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44256b32bdc7768b58a475fb227f8821314f4c1d
--- /dev/null
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::ObjectRenderer do
+  let(:project) { create(:empty_project) }
+  let(:user) { project.owner }
+
+  describe '#render' do
+    it 'renders and redacts an Array of objects' do
+      renderer = described_class.new(project, user)
+      object = double(:object, note: 'hello', note_html: nil)
+
+      expect(renderer).to receive(:render_objects).with([object], :note).
+        and_call_original
+
+      expect(renderer).to receive(:redact_documents).
+        with(an_instance_of(Array)).
+        and_call_original
+
+      expect(object).to receive(:note_html=).with('<p>hello</p>')
+
+      renderer.render([object], :note)
+    end
+  end
+
+  describe '#render_objects' do
+    it 'renders an Array of objects' do
+      object = double(:object, note: 'hello')
+      renderer = described_class.new(project, user)
+
+      expect(renderer).to receive(:render_attribute).with(object, :note).
+        and_call_original
+
+      rendered = renderer.render_objects([object], :note)
+
+      expect(rendered).to be_an_instance_of(Array)
+      expect(rendered[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+    end
+  end
+
+  describe '#redact_documents' do
+    it 'redacts a set of documents and returns them as an Array of Strings' do
+      doc = Nokogiri::HTML.fragment('<p>hello</p>')
+      renderer = described_class.new(project, user)
+
+      expect_any_instance_of(Banzai::Redactor).to receive(:redact).
+        with([doc]).
+        and_call_original
+
+      redacted = renderer.redact_documents([doc])
+
+      expect(redacted).to eq(['<p>hello</p>'])
+    end
+  end
+
+  describe '#context_for' do
+    let(:object) { double(:object, note: 'hello') }
+    let(:renderer) { described_class.new(project, user) }
+
+    it 'returns a Hash' do
+      expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
+    end
+
+    it 'includes the cache key' do
+      context = renderer.context_for(object, :note)
+
+      expect(context[:cache_key]).to eq([object, :note])
+    end
+
+    context 'when the object responds to "author"' do
+      it 'includes the author in the context' do
+        expect(object).to receive(:author).and_return('Alice')
+
+        context = renderer.context_for(object, :note)
+
+        expect(context[:author]).to eq('Alice')
+      end
+    end
+
+    context 'when the object does not respond to "author"' do
+      it 'does not include the author in the context' do
+        context = renderer.context_for(object, :note)
+
+        expect(context.key?(:author)).to eq(false)
+      end
+    end
+  end
+
+  describe '#render_attribute' do
+    it 'renders the attribute of an object' do
+      object = double(:doc, note: 'hello')
+      renderer = described_class.new(project, user, pipeline: :note)
+      doc = renderer.render_attribute(object, :note)
+
+      expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+      expect(doc.to_html).to eq('<p>hello</p>')
+    end
+  end
+
+  describe '#base_context' do
+    let(:context) do
+      described_class.new(project, user, pipeline: :note).base_context
+    end
+
+    it 'returns a Hash' do
+      expect(context).to be_an_instance_of(Hash)
+    end
+
+    it 'includes the custom attributes' do
+      expect(context[:pipeline]).to eq(:note)
+    end
+
+    it 'includes the current user' do
+      expect(context[:current_user]).to eq(user)
+    end
+
+    it 'includes the current project' do
+      expect(context[:project]).to eq(project)
+    end
+  end
+end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..488f465bcdaf181f3257bcafdbfd4a2a9ad3416f
--- /dev/null
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Banzai::Redactor do
+  let(:user) { build(:user) }
+  let(:project) { build(:empty_project) }
+  let(:redactor) { described_class.new(project, user) }
+
+  describe '#redact' do
+    it 'redacts an Array of documents' do
+      doc1 = Nokogiri::HTML.
+        fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+
+      doc2 = Nokogiri::HTML.
+        fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+
+      expect(redactor).to receive(:nodes_visible_to_user).and_return([])
+
+      expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2])
+
+      expect(doc1.to_html).to eq('foo')
+      expect(doc2.to_html).to eq('bar')
+    end
+  end
+
+  describe '#redact_nodes' do
+    it 'redacts an Array of nodes' do
+      doc = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+      node = doc.children[0]
+
+      expect(redactor).to receive(:nodes_visible_to_user).
+        with([node]).
+        and_return(Set.new)
+
+      redactor.redact_nodes([node])
+
+      expect(doc.to_html).to eq('foo')
+    end
+  end
+
+  describe '#nodes_visible_to_user' do
+    it 'returns a Set containing the visible nodes' do
+      doc = Nokogiri::HTML.fragment('<a data-reference-type="issue"></a>')
+      node = doc.children[0]
+
+      expect_any_instance_of(Banzai::ReferenceParser::IssueParser).
+        to receive(:nodes_visible_to_user).
+        with(user, [node]).
+        and_return([node])
+
+      expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node]))
+    end
+  end
+end