diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 297cf2eaf1478873488b3602350f197ce4746dfd..ce11c43d523758fdd00a1f16a6598ac31ab58a98 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -97,4 +97,8 @@ pre.unformatted {
 .badge.is-tag.is-small {
   padding: 2px 4px;
   line-height: 1;
+}
+
+.badge.is-tag.is-large {
+  font-size: 22px;
 }
\ No newline at end of file
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 10b62d4af81a9df514a3b8a6f3a7d0ce86479038..a9ebd1b73d5c5e87edc90bbef9a18c4f4b44ab8d 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -1,4 +1,8 @@
 class TagsController < ApplicationController
+  before_action :authenticate_user!, only: [:edit, :update]
+  before_action :set_category, except: [:index]
+  before_action :set_tag, only: [:show, :edit, :update]
+
   def index
     @tag_set = if params[:tag_set].present?
                  TagSet.find(params[:tag_set])
@@ -6,12 +10,49 @@ class TagsController < ApplicationController
     @tags = if params[:term].present?
               (@tag_set&.tags || Tag).search(params[:term])
             else
-              @tag_set&.tags || Tag.all
-            end.order(:name).paginate(page: params[:page], per_page: 50)
+              (@tag_set&.tags|| Tag.all).order(:name)
+            end.paginate(page: params[:page], per_page: 50)
     respond_to do |format|
       format.json do
         render json: @tags
       end
     end
   end
+
+  def category
+    @tag_set = @category.tag_set
+    @tags = if params[:q].present?
+              @tag_set.tags.search(params[:q])
+            else
+              @tag_set.tags.left_joins(:posts).group(Arel.sql('tags.id')).order(Arel.sql('COUNT(posts.id) DESC'))
+                      .select(Arel.sql('tags.*, COUNT(posts.id) AS post_count'))
+            end
+  end
+
+  def show; end
+
+  def edit
+
+  end
+
+  def update
+
+  end
+
+  private
+
+  def set_tag
+    @tag = Tag.find params[:tag_id]
+    required_ids = @category&.required_tag_ids
+    moderator_ids = @category&.moderator_tag_ids
+    topic_ids = @category&.topic_tag_ids
+    required = required_ids&.include?(@tag.id) ? 'is-filled' : ''
+    topic = topic_ids&.include?(@tag.id) ? 'is-outlined' : ''
+    moderator = moderator_ids&.include?(@tag.id) ? 'is-red is-outlined' : ''
+    @classes = "badge is-tag #{required} #{topic} #{moderator}"
+  end
+
+  def set_category
+    @category = Category.find params[:id]
+  end
 end
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index c8d534d6fbd02335170dd1c8148c013f80f07feb..7224375733148d345cad15261fc326955356f345 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -112,7 +112,9 @@
       <div class="category-header--name"><%= current_cat.name %></div>
       <div class="category-header--nav">
         <%= link_to 'Posts', category_path(current_cat),
-                    class: "category-header--nav-item #{active?(current_cat) ? 'is-active' : ''}" %>
+                    class: "category-header--nav-item #{active?(current_cat) && controller_name != 'tags' ? 'is-active' : ''}" %>
+        <%= link_to 'Tags', category_tags_path(current_cat),
+                    class: "category-header--nav-item #{active?(current_cat) && controller_name == 'tags' ? 'is-active' : ''}" %>
         <%# There will eventually be a Tags link here too, once we have a page that lists tags per-tag set. %>
         <div class="category-header--nav-separator"></div>
         <% ptid = current_cat.display_post_types.reject { |e| e.to_s.empty? }.first || Question.post_type_id %>
diff --git a/app/views/tags/_tag.html.erb b/app/views/tags/_tag.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..27691270f81f202bb1295ff3ec4a015d517898cb
--- /dev/null
+++ b/app/views/tags/_tag.html.erb
@@ -0,0 +1,9 @@
+<div class="grid--cell is-4-lg is-6-md is-12-sm">
+  <%= link_to tag.name, tag_path(id: category.id, tag_id: tag.id), class: classes %>
+  <span class="has-color-tertiary-900">&times; <%= tag.post_count %></span>
+  <% if tag.excerpt.present? %>
+    <p class="has-font-size-caption">
+      <%= split_words_max_length(strip_markdown(tag.excerpt_markdown), 150) %>
+    </p>
+  <% end %>
+</div>
\ No newline at end of file
diff --git a/app/views/tags/category.html.erb b/app/views/tags/category.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..32f8fd292e9ad58b2709560119b51f8c3a74a4e8
--- /dev/null
+++ b/app/views/tags/category.html.erb
@@ -0,0 +1,17 @@
+<% content_for :title, "Tags for #{@category.name}" %>
+
+<h1>Tags for <%= @category.name %></h1>
+
+<div class="grid">
+  <% required_ids = @category&.required_tag_ids %>
+  <% moderator_ids = @category&.moderator_tag_ids %>
+  <% topic_ids = @category&.topic_tag_ids %>
+
+  <% @tags.each do |tag| %>
+    <% required = required_ids&.include?(tag.id) ? 'is-filled' : '' %>
+    <% topic = topic_ids&.include?(tag.id) ? 'is-outlined' : '' %>
+    <% moderator = moderator_ids&.include?(tag.id) ? 'is-red is-outlined' : '' %>
+    <% classes = "badge is-tag #{required} #{topic} #{moderator}" %>
+    <%= render 'tag', category: @category, tag: tag, classes: classes %>
+  <% end %>
+</div>
diff --git a/app/views/tags/show.html.erb b/app/views/tags/show.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..26ce3b49e18456c2a7da0b1f23fed04b64ee695c
--- /dev/null
+++ b/app/views/tags/show.html.erb
@@ -0,0 +1 @@
+<h1>Tag: <span class="<%= @classes %> is-large"><%= @tag.name %></span></h1>
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 6f84a49bd62dd24e4c105f9fee4f2d37f94e26f5..52a026f19b0af84a4465dd060efe0c12fa33c196 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -181,6 +181,10 @@ Rails.application.routes.draw do
     post   ':id/edit',                             to: 'categories#update', as: :update_category
     delete ':id',                                  to: 'categories#destroy', as: :destroy_category
     get    ':id/feed',                             to: 'categories#rss_feed', as: :category_feed
+    get    ':id/tags',                             to: 'tags#category', as: :category_tags
+    get    ':id/tags/:tag_id',                       to: 'tags#show', as: :tag
+    get    ':id/tags/:tag_id/edit',                  to: 'tags#edit', as: :edit_tag
+    patch  ':id/tags/:tag_id/edit',                  to: 'tags#update', as: :update_tag
   end
 
   get   'warning',                         to: 'mod_warning#current', as: :current_mod_warning
diff --git a/db/migrate/20200629131408_update_tag_model.rb b/db/migrate/20200629131408_update_tag_model.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00d9f2ecc079b4bae53cff19b72f89c0c2a18ea4
--- /dev/null
+++ b/db/migrate/20200629131408_update_tag_model.rb
@@ -0,0 +1,9 @@
+class UpdateTagModel < ActiveRecord::Migration[5.2]
+  def change
+    remove_column :tags, :description
+    add_column :tags, :wiki_markdown, :text
+    add_column :tags, :excerpt_markdown, :text
+    add_column :tags, :wiki, :text
+    add_column :tags, :excerpt, :text
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 78959a5a2a15d4f48ba07b41979767fe775bfeab..fe14f492fbe26fcedfbd577d0bc54ed2fbca5052 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2020_06_25_115618) do
+ActiveRecord::Schema.define(version: 2020_06_29_131408) do
 
   create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t|
     t.string "name", null: false
@@ -359,11 +359,14 @@ ActiveRecord::Schema.define(version: 2020_06_25_115618) do
 
   create_table "tags", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t|
     t.string "name"
-    t.text "description"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
     t.bigint "community_id", null: false
     t.bigint "tag_set_id", null: false
+    t.text "wiki_markdown"
+    t.text "excerpt_markdown"
+    t.text "wiki"
+    t.text "excerpt"
     t.index ["community_id"], name: "index_tags_on_community_id"
     t.index ["tag_set_id"], name: "index_tags_on_tag_set_id"
   end