diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr
index 0018e5c9..f32d457d 100644
--- a/src/invidious/channels.cr
+++ b/src/invidious/channels.cr
@@ -380,24 +380,73 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by)
   return items, continuation
 end
 
-def fetch_channel_featured_channels(ucid, tab_data, params = nil, continuation = nil, title = nil)
+def fetch_channel_featured_channels(ucid, tab_data, params = nil, continuation = nil, query_title = nil)
   if continuation.is_a?(String)
     initial_data = request_youtube_api_browse(continuation)
-    channels_tab_content = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"]
+    items = extract_items(initial_data)
+    continuation_token = fetch_continuation_token(initial_data)
 
-    return process_featured_channels([channels_tab_content], nil, title, continuation_items = true)
+    return [Category.new({
+      title:                query_title.not_nil!, # If continuation contents is requested then the query_title has to be passed along.
+      contents:             items,
+      browse_endpoint_data: nil,
+      continuation_token:   continuation_token,
+      badges:               nil,
+    })]
   else
     if params.is_a?(String)
       initial_data = request_youtube_api_browse(ucid, params)
+      continuation_token = fetch_continuation_token(initial_data)
     else
       initial_data = request_youtube_api_browse(ucid, tab_data[1])
+      continuation_token = nil
     end
 
-    channels_tab = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][tab_data[0]]["tabRenderer"]
-    channels_tab_content = channels_tab["content"]["sectionListRenderer"]["contents"].as_a
-    submenu_data = channels_tab["content"]["sectionListRenderer"]["subMenu"]?.try &.["channelSubMenuRenderer"]["contentTypeSubMenuItems"] || false
+    channels_tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])
+    submenu = channels_tab["content"]["sectionListRenderer"]["subMenu"]?
 
-    return process_featured_channels(channels_tab_content, submenu_data)
+    # There's no submenu data if the channel doesn't feature any channels.
+    if !submenu
+      return [] of Category
+    end
+
+    submenu_data = submenu["channelSubMenuRenderer"]["contentTypeSubMenuItems"]
+      
+    items = extract_items(initial_data)
+    fallback_title = submenu_data.as_a.select(&.["selected"].as_bool)[0]["title"].as_s
+
+    # Although extract_items parsed everything into the right structs, we still have
+    # to fill in the title (if missing) attribute since Youtube doesn't return it when requesting
+    # a full category
+
+    category_array = [] of Category
+    items.each do |category|
+      # Tell compiler that the result from extract_items has to be an array of Categories
+      if !category.is_a?(Category)
+        next
+      end
+
+      category_array << Category.new({
+        title:                category.title.empty? ? fallback_title : category.title,
+        contents:             category.contents,
+        browse_endpoint_data: category.browse_endpoint_data,
+        continuation_token:   continuation_token,
+        badges:               nil,
+      })
+    end
+
+    # If we don't have any categories we'll create one.
+    if category_array.empty?
+      return [Category.new({
+        title:                fallback_title, # If continuation contents is requested then the query_title has to be passed along.
+        contents:             items,
+        browse_endpoint_data: nil,
+        continuation_token:   continuation_token,
+        badges:               nil,
+      })]
+    end
+
+    return category_array
   end
 end
 
diff --git a/src/invidious/featured_channels.cr b/src/invidious/featured_channels.cr
deleted file mode 100644
index e1486403..00000000
--- a/src/invidious/featured_channels.cr
+++ /dev/null
@@ -1,170 +0,0 @@
-struct FeaturedChannel
-  include DB::Serializable
-
-  property author : String
-  property ucid : String
-  property author_thumbnail : String
-  property subscriber_count : Int32
-  property video_count : Int32
-  property description_html : String?
-
-  def to_json(locale, json : JSON::Builder)
-    json.object do
-      json.field "author", self.author
-      json.field "authorId", self.ucid
-      json.field "authorUrl", "/channel/#{self.ucid}"
-      json.field "authorThumbnails" do
-        json.array do
-          qualities = {32, 48, 76, 100, 176, 512}
-
-          qualities.each do |quality|
-            json.object do
-              json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
-              json.field "width", quality
-              json.field "height", quality
-            end
-          end
-        end
-      end
-
-      json.field "description", html_to_content(self.description_html)
-      json.field "descriptionHtml", self.description_html
-      json.field "subCount", self.subscriber_count
-      json.field "videoCount", self.video_count
-      json.field "badges", self.badges
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder | Nil = nil)
-    if json
-      to_json(locale, json)
-    else
-      JSON.build do |json|
-        to_json(locale, json)
-      end
-    end
-  end
-end
-
-struct Category
-  include DB::Serializable
-
-  property title : String
-  property contents : Array(FeaturedChannel) | FeaturedChannel
-  property browse_endpoint_param : String?
-  property continuation_token : String?
-
-  def to_json(locale, json : JSON::Builder)
-    json.object do
-      json.field "title", self.title
-      json.field "contents", self.contents
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder | Nil = nil)
-    if json
-      to_json(locale, json)
-    else
-      JSON.build do |json|
-        to_json(locale, json)
-      end
-    end
-  end
-end
-
-def _extract_channel_data(channel)
-  ucid = channel["channelId"].as_s
-  author = channel["title"]["simpleText"].as_s
-  author_thumbnail = channel["thumbnail"]["thumbnails"].as_a[0]["url"].as_s
-  subscriber_count = channel["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s?
-    .try { |text| short_text_to_number(text.split(" ")[0]) } || 0
-
-  video_count = channel["videoCountText"]?.try &.["runs"][0]["text"].as_s.gsub(/\D/, "").to_i || 0
-
-  if channel["descriptionSnippet"]?
-    description = channel["descriptionSnippet"]["runs"][0]["text"].as_s
-    description_html = HTML.escape(description).gsub("\n", "")
-  else
-    description_html = nil
-  end
-
-  FeaturedChannel.new({
-    author:           author,
-    ucid:             ucid,
-    author_thumbnail: author_thumbnail,
-    subscriber_count: subscriber_count,
-    video_count:      video_count,
-    description_html: description_html,
-  })
-end
-
-def process_featured_channels(data, submenu_data, title = nil, continuation_items = false)
-  all_categories = [] of Category
-
-  if submenu_data.is_a?(Bool)
-    return all_categories
-  end
-
-  # Extraction process differs when there's more than one category
-  if data.size > 1
-    data.each do |raw_category|
-      raw_category = raw_category["itemSectionRenderer"]["contents"].as_a[0]["shelfRenderer"]
-
-      category_title = raw_category["title"]["runs"][0]["text"].as_s
-      browse_endpoint_param = raw_category["endpoint"]["browseEndpoint"]["params"].as_s
-
-      # Category has multiple channels
-      if raw_category["content"].as_h.has_key?("horizontalListRenderer")
-        contents = [] of FeaturedChannel
-        raw_category["content"]["horizontalListRenderer"]["items"].as_a.each do |channel|
-          contents << _extract_channel_data(channel["gridChannelRenderer"])
-        end
-        # Single channel
-      else
-        channel = raw_category["content"]["expandedShelfContentsRenderer"]["items"][0]["channelRenderer"]
-        contents = _extract_channel_data(channel)
-      end
-
-      all_categories << Category.new({
-        title:                 category_title,
-        contents:              contents,
-        browse_endpoint_param: browse_endpoint_param,
-        continuation_token:    nil,
-      })
-    end
-  else
-    if !continuation_items
-      raw_category_contents = data[0]["itemSectionRenderer"]["contents"].as_a[0]["gridRenderer"]["items"].as_a
-    else
-      raw_category_contents = data[0].as_a
-    end
-
-    category_title = submenu_data.try &.[0]["title"].as_s || title || ""
-
-    browse_endpoint_param = nil # Not needed
-    continuation_token = nil
-
-    # If a continuation token is needed it'll always be after at least twelve channels
-    if raw_category_contents.size > 12
-      continuation_token = raw_category_contents[-1]["continuationItemRenderer"]?.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s || nil
-
-      if !continuation_token.nil?
-        raw_category_contents = raw_category_contents[0..-2]
-      end
-    end
-
-    contents = [] of FeaturedChannel
-    raw_category_contents.each do |channel|
-      contents << _extract_channel_data(channel["gridChannelRenderer"])
-    end
-
-    all_categories << Category.new({
-      title:                 category_title,
-      contents:              contents,
-      browse_endpoint_param: browse_endpoint_param,
-      continuation_token:    continuation_token,
-    })
-  end
-
-  return all_categories
-end
diff --git a/src/invidious/helpers/extractors.cr b/src/invidious/helpers/extractors.cr
index 6e16c879..a9523eb8 100644
--- a/src/invidious/helpers/extractors.cr
+++ b/src/invidious/helpers/extractors.cr
@@ -1,11 +1,11 @@
-# This file contains helper methods to parse the Youtube API json data into 
+# This file contains helper methods to parse the Youtube API json data into
 # neat little packages we can use
 
 # Tuple of Parsers/Extractors so we can easily cycle through them.
 private ITEM_CONTAINER_EXTRACTOR = {
   YoutubeTabsExtractor.new,
   SearchResultsExtractor.new,
-  ContinuationExtractor.new
+  ContinuationExtractor.new,
 }
 
 private ITEM_PARSERS = {
@@ -13,6 +13,7 @@ private ITEM_PARSERS = {
   ChannelParser.new,
   GridPlaylistParser.new,
   PlaylistParser.new,
+  CategoryParser.new,
 }
 
 private struct AuthorFallback
@@ -33,7 +34,7 @@ private class ItemParser
 
   private def parse(item_contents : JSON::Any, author_fallback : AuthorFallback)
   end
-end 
+end
 
 private class VideoParser < ItemParser
   def process(item, author_fallback)
@@ -98,7 +99,7 @@ end
 
 private class ChannelParser < ItemParser
   def process(item, author_fallback)
-    if item_contents = item["channelRenderer"]?
+    if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
       return self.parse(item_contents, author_fallback)
     end
   end
@@ -197,7 +198,89 @@ private class PlaylistParser < ItemParser
   end
 end
 
-# The following are the extractors for extracting an array of items from 
+private class CategoryParser < ItemParser
+  def process(item, author_fallback)
+    if item_contents = item["shelfRenderer"]?
+      return self.parse(item_contents, author_fallback)
+    end
+  end
+
+  def parse(item_contents, author_fallback)
+    # Title extraction is a bit complicated. There are two possible routes for it
+    # as well as times when the title attribute just isn't sent by YT.
+
+    title_container = item_contents["title"]? || ""
+    if !title_container.is_a? String
+      if title = title_container["simpleText"]?
+        title = title.as_s
+      else
+        title = title_container["runs"][0]["text"].as_s
+      end
+    else
+      title = ""
+    end
+
+    browse_endpoint = item_contents["endpoint"]?.try &.["browseEndpoint"] || nil
+    browse_endpoint_data = ""
+    category_type = 0 # 0: Video, 1: Channels, 2: Playlist/feed, 3: trending
+
+    # There's no endpoint data for video and trending category
+    if !item_contents["endpoint"]?
+      if !item_contents["videoId"]?
+        category_type = 3
+      end
+    end
+
+    if !browse_endpoint.nil?
+      # Playlist/feed categories doesn't need the params value (nor is it even included in yt response)
+      # instead it uses the browseId parameter. So if there isn't a params value we can assume the
+      # category is a playlist/feed
+      if browse_endpoint["params"]?
+        browse_endpoint_data = browse_endpoint["params"].as_s
+        category_type = 1
+      else
+        browse_endpoint_data = browse_endpoint["browseId"].as_s
+        category_type = 2
+      end
+    end
+
+    # Sometimes a category can have badges.
+    badges = [] of Tuple(String, String) # (Badge style, label)
+    item_contents["badges"]?.try &.as_a.each do |badge|
+      badge = badge["metadataBadgeRenderer"]
+      badges << {badge["style"].as_s, badge["label"].as_s}
+    end
+
+    # Content parsing
+    contents = [] of SearchItem
+
+    # Content could be in three locations.
+    if content_container = item_contents["content"]["horizontalListRenderer"]?
+    elsif content_container = item_contents["content"]["expandedShelfContentsRenderer"]
+    elsif content_container = item_contents["content"]["verticalListRenderer"]
+    else
+      content_container = item_contents["contents"]
+    end
+
+    raw_contents = content_container["items"].as_a
+    raw_contents.each do |item|
+      result = extract_item(item)
+      if !result.nil?
+        contents << result
+      end
+    end
+
+    Category.new({
+      title:                title,
+      contents:             contents,
+      browse_endpoint_data: browse_endpoint_data,
+      continuation_token:   nil,
+      badges:               badges,
+    })
+  end
+end
+
+# The following are the extractors for extracting an array of items from
 # the internal Youtube API's JSON response. The result is then packaged into
 # a structure we can more easily use via the parsers above. Their internals are
 # identical to the item parsers.
@@ -220,25 +303,22 @@ private class YoutubeTabsExtractor < ItemsContainerExtractor
   private def extract(target)
     raw_items = [] of JSON::Any
     selected_tab = extract_selected_tab(target["tabs"])
-    content = selected_tab["tabRenderer"]["content"]
+    content = selected_tab["content"]
 
-    content["sectionListRenderer"]["contents"].as_a.each do | renderer_container |
+    content["sectionListRenderer"]["contents"].as_a.each do |renderer_container|
       renderer_container = renderer_container["itemSectionRenderer"]
       renderer_container_contents = renderer_container["contents"].as_a[0]
 
-      # Shelf renderer usually refer to a category and would need special handling once
-      # An extractor for categories are added. But for now it is just used to 
-      # extract items for the trending page
+      # Category extraction
       if items_container = renderer_container_contents["shelfRenderer"]?
-        if items_container["content"]["expandedShelfContentsRenderer"]?
-           items_container = items_container["content"]["expandedShelfContentsRenderer"]
-        end
-      elsif items_container = renderer_container_contents["gridRenderer"]? 
+        raw_items << renderer_container_contents
+        next
+      elsif items_container = renderer_container_contents["gridRenderer"]?
       else
         items_container = renderer_container_contents
       end
 
-      items_container["items"].as_a.each do | item |
+      items_container["items"].as_a.each do |item|
         raw_items << item
       end
     end
@@ -268,6 +348,8 @@ private class ContinuationExtractor < ItemsContainerExtractor
   def process(initial_data)
     if target = initial_data["continuationContents"]?
       self.extract(target)
+    elsif target = initial_data["appendContinuationItemsAction"]?
+      self.extract(target)
     end
   end
 
@@ -275,20 +357,23 @@ private class ContinuationExtractor < ItemsContainerExtractor
     raw_items = [] of JSON::Any
     if content = target["gridContinuation"]?
       raw_items = content["items"].as_a
+    elsif content = target["continuationItems"]?
+      raw_items = content.as_a
     end
 
     return raw_items
   end
 end
 
-def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fallback : String? = nil)
+def extract_item(item : JSON::Any, author_fallback : String? = nil,
+                 author_id_fallback : String? = nil)
   # Parses an item from Youtube's JSON response into a more usable structure.
   # The end result can either be a SearchVideo, SearchPlaylist or SearchChannel.
   author_fallback = AuthorFallback.new(author_fallback, author_id_fallback)
 
   # Cycles through all of the item parsers and attempt to parse the raw YT JSON data.
-  # Each parser automatically validates the data given to see if the data is 
-  # applicable to itself. If not nil is returned and the next parser is attemped. 
+  # Each parser automatically validates the data given to see if the data is
+  # applicable to itself. If not nil is returned and the next parser is attemped.
   ITEM_PARSERS.each do |parser|
     result = parser.process(item, author_fallback)
     if !result.nil?
@@ -298,23 +383,31 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa
   # TODO radioRenderer, showRenderer, shelfRenderer, horizontalCardListRenderer, searchPyvRenderer
 end
 
-def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
+def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil,
+                  author_id_fallback : String? = nil)
   items = [] of SearchItem
-  initial_data = initial_data["contents"]?.try &.as_h  || initial_data["response"]?.try &.as_h || initial_data
+
+  if unpackaged_data = initial_data["contents"]?.try &.as_h
+  elsif unpackaged_data = initial_data["response"]?.try &.as_h
+  elsif unpackaged_data = initial_data["onResponseReceivedActions"]?.try &.as_a.[0].as_h
+  else
+    unpackaged_data = initial_data
+  end
 
   # This is identicial to the parser cyling of extract_item().
-  ITEM_CONTAINER_EXTRACTOR.each do | extractor |
-    results = extractor.process(initial_data)
+  ITEM_CONTAINER_EXTRACTOR.each do |extractor|
+    results = extractor.process(unpackaged_data)
     if !results.nil?
-      results.each do | item |
+      results.each do |item|
         parsed_result = extract_item(item, author_fallback, author_id_fallback)
 
         if !parsed_result.nil?
           items << parsed_result
         end
       end
+      return items      
     end
   end
 
   return items
-end
\ No newline at end of file
+end
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 7c234f3c..7d687567 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -248,12 +248,38 @@ def html_to_content(description_html : String)
 end
 
 def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
-  extract_items(initial_data, author_fallback, author_id_fallback).select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
+  extracted = extract_items(initial_data, author_fallback, author_id_fallback)
+
+  if extracted.is_a?(Category)
+    target = extracted.contents
+  else
+    target = extracted
+  end
+  return target.select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
 end
 
 def extract_selected_tab(tabs)
   # Extract the selected tab from the array of tabs Youtube returns
-  return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]
+  return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"]
+end
+
+def fetch_continuation_token(items : Array(JSON::Any))
+  # Fetches the continuation token from an array of items
+  return items.last["continuationItemRenderer"]?
+  .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
+
+end
+
+def fetch_continuation_token(initial_data : Hash(String, JSON::Any))
+  # Fetches the continuation token from initial data
+  if initial_data["onResponseReceivedActions"]?
+    continuation_items = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"]
+  else
+    tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])
+    continuation_items = tab["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["gridRenderer"]["items"]
+  end
+
+  return fetch_continuation_token(continuation_items.as_a)
 end
 
 def check_enum(db, enum_name, struct_type = nil)
diff --git a/src/invidious/helpers/invidiousitems.cr b/src/invidious/helpers/invidiousitems.cr
new file mode 100644
index 00000000..8694ae97
--- /dev/null
+++ b/src/invidious/helpers/invidiousitems.cr
@@ -0,0 +1,258 @@
+struct SearchVideo
+  include DB::Serializable
+
+  property title : String
+  property id : String
+  property author : String
+  property ucid : String
+  property published : Time
+  property views : Int64
+  property description_html : String
+  property length_seconds : Int32
+  property live_now : Bool
+  property paid : Bool
+  property premium : Bool
+  property premiere_timestamp : Time?
+
+  def to_xml(auto_generated, query_params, xml : XML::Builder)
+    query_params["v"] = self.id
+
+    xml.element("entry") do
+      xml.element("id") { xml.text "yt:video:#{self.id}" }
+      xml.element("yt:videoId") { xml.text self.id }
+      xml.element("yt:channelId") { xml.text self.ucid }
+      xml.element("title") { xml.text self.title }
+      xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
+
+      xml.element("author") do
+        if auto_generated
+          xml.element("name") { xml.text self.author }
+          xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
+        else
+          xml.element("name") { xml.text author }
+          xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
+        end
+      end
+
+      xml.element("content", type: "xhtml") do
+        xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
+          xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
+            xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
+          end
+
+          xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
+        end
+      end
+
+      xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
+
+      xml.element("media:group") do
+        xml.element("media:title") { xml.text self.title }
+        xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
+          width: "320", height: "180")
+        xml.element("media:description") { xml.text html_to_content(self.description_html) }
+      end
+
+      xml.element("media:community") do
+        xml.element("media:statistics", views: self.views)
+      end
+    end
+  end
+
+  def to_xml(auto_generated, query_params, xml : XML::Builder | Nil = nil)
+    if xml
+      to_xml(HOST_URL, auto_generated, query_params, xml)
+    else
+      XML.build do |json|
+        to_xml(HOST_URL, auto_generated, query_params, xml)
+      end
+    end
+  end
+
+  def to_json(locale : Hash(String, JSON::Any), json : JSON::Builder)
+    json.object do
+      json.field "type", "video"
+      json.field "title", self.title
+      json.field "videoId", self.id
+
+      json.field "author", self.author
+      json.field "authorId", self.ucid
+      json.field "authorUrl", "/channel/#{self.ucid}"
+
+      json.field "videoThumbnails" do
+        generate_thumbnails(json, self.id)
+      end
+
+      json.field "description", html_to_content(self.description_html)
+      json.field "descriptionHtml", self.description_html
+
+      json.field "viewCount", self.views
+      json.field "published", self.published.to_unix
+      json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
+      json.field "lengthSeconds", self.length_seconds
+      json.field "liveNow", self.live_now
+      json.field "paid", self.paid
+      json.field "premium", self.premium
+      json.field "isUpcoming", self.is_upcoming
+
+      if self.premiere_timestamp
+        json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
+      end
+    end
+  end
+
+  def to_json(locale, json : JSON::Builder | Nil = nil)
+    if json
+      to_json(locale, json)
+    else
+      JSON.build do |json|
+        to_json(locale, json)
+      end
+    end
+  end
+
+  def is_upcoming
+    premiere_timestamp ? true : false
+  end
+end
+
+struct SearchPlaylistVideo
+  include DB::Serializable
+
+  property title : String
+  property id : String
+  property length_seconds : Int32
+end
+
+struct SearchPlaylist
+  include DB::Serializable
+
+  property title : String
+  property id : String
+  property author : String
+  property ucid : String
+  property video_count : Int32
+  property videos : Array(SearchPlaylistVideo)
+  property thumbnail : String?
+
+  def to_json(locale, json : JSON::Builder)
+    json.object do
+      json.field "type", "playlist"
+      json.field "title", self.title
+      json.field "playlistId", self.id
+      json.field "playlistThumbnail", self.thumbnail
+
+      json.field "author", self.author
+      json.field "authorId", self.ucid
+      json.field "authorUrl", "/channel/#{self.ucid}"
+
+      json.field "videoCount", self.video_count
+      json.field "videos" do
+        json.array do
+          self.videos.each do |video|
+            json.object do
+              json.field "title", video.title
+              json.field "videoId", video.id
+              json.field "lengthSeconds", video.length_seconds
+
+              json.field "videoThumbnails" do
+                generate_thumbnails(json, video.id)
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+
+  def to_json(locale, json : JSON::Builder | Nil = nil)
+    if json
+      to_json(locale, json)
+    else
+      JSON.build do |json|
+        to_json(locale, json)
+      end
+    end
+  end
+end
+
+struct SearchChannel
+  include DB::Serializable
+
+  property author : String
+  property ucid : String
+  property author_thumbnail : String
+  property subscriber_count : Int32
+  property video_count : Int32
+  property description_html : String
+  property auto_generated : Bool
+
+  def to_json(locale, json : JSON::Builder)
+    json.object do
+      json.field "type", "channel"
+      json.field "author", self.author
+      json.field "authorId", self.ucid
+      json.field "authorUrl", "/channel/#{self.ucid}"
+
+      json.field "authorThumbnails" do
+        json.array do
+          qualities = {32, 48, 76, 100, 176, 512}
+
+          qualities.each do |quality|
+            json.object do
+              json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
+              json.field "width", quality
+              json.field "height", quality
+            end
+          end
+        end
+      end
+
+      json.field "autoGenerated", self.auto_generated
+      json.field "subCount", self.subscriber_count
+      json.field "videoCount", self.video_count
+
+      json.field "description", html_to_content(self.description_html)
+      json.field "descriptionHtml", self.description_html
+    end
+  end
+
+  def to_json(locale, json : JSON::Builder | Nil = nil)
+    if json
+      to_json(locale, json)
+    else
+      JSON.build do |json|
+        to_json(locale, json)
+      end
+    end
+  end
+end
+
+class Category
+  include DB::Serializable
+
+  property title : String
+  property contents : Array(SearchItem) | SearchItem
+  property browse_endpoint_data : String?
+  property continuation_token : String?
+  property badges : Array(Tuple(String, String))?
+
+  def to_json(locale, json : JSON::Builder)
+    json.object do
+      json.field "title", self.title
+      json.field "contents", self.contents
+    end
+  end
+
+  def to_json(locale, json : JSON::Builder | Nil = nil)
+    if json
+      to_json(locale, json)
+    else
+      JSON.build do |json|
+        to_json(locale, json)
+      end
+    end
+  end
+end
+
+alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category
diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr
index d96f7c46..3d64d796 100644
--- a/src/invidious/routes/channels.cr
+++ b/src/invidious/routes/channels.cr
@@ -102,7 +102,6 @@ class Invidious::Routes::Channels < Invidious::Routes::BaseRoute
       return env.redirect "/channel/#{channel.ucid}"
     end
 
-    # When a channel only has a single category it lacks the category param option so we'll handle it here.
     if continuation
       offset = env.params.query["offset"]?
       if offset
diff --git a/src/invidious/search.cr b/src/invidious/search.cr
index 6d4afc03..60d95bcd 100644
--- a/src/invidious/search.cr
+++ b/src/invidious/search.cr
@@ -1,235 +1,3 @@
-struct SearchVideo
-  include DB::Serializable
-
-  property title : String
-  property id : String
-  property author : String
-  property ucid : String
-  property published : Time
-  property views : Int64
-  property description_html : String
-  property length_seconds : Int32
-  property live_now : Bool
-  property paid : Bool
-  property premium : Bool
-  property premiere_timestamp : Time?
-
-  def to_xml(auto_generated, query_params, xml : XML::Builder)
-    query_params["v"] = self.id
-
-    xml.element("entry") do
-      xml.element("id") { xml.text "yt:video:#{self.id}" }
-      xml.element("yt:videoId") { xml.text self.id }
-      xml.element("yt:channelId") { xml.text self.ucid }
-      xml.element("title") { xml.text self.title }
-      xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
-
-      xml.element("author") do
-        if auto_generated
-          xml.element("name") { xml.text self.author }
-          xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
-        else
-          xml.element("name") { xml.text author }
-          xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
-        end
-      end
-
-      xml.element("content", type: "xhtml") do
-        xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
-          xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
-            xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
-          end
-
-          xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
-        end
-      end
-
-      xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
-
-      xml.element("media:group") do
-        xml.element("media:title") { xml.text self.title }
-        xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
-          width: "320", height: "180")
-        xml.element("media:description") { xml.text html_to_content(self.description_html) }
-      end
-
-      xml.element("media:community") do
-        xml.element("media:statistics", views: self.views)
-      end
-    end
-  end
-
-  def to_xml(auto_generated, query_params, xml : XML::Builder | Nil = nil)
-    if xml
-      to_xml(HOST_URL, auto_generated, query_params, xml)
-    else
-      XML.build do |json|
-        to_xml(HOST_URL, auto_generated, query_params, xml)
-      end
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder)
-    json.object do
-      json.field "type", "video"
-      json.field "title", self.title
-      json.field "videoId", self.id
-
-      json.field "author", self.author
-      json.field "authorId", self.ucid
-      json.field "authorUrl", "/channel/#{self.ucid}"
-
-      json.field "videoThumbnails" do
-        generate_thumbnails(json, self.id)
-      end
-
-      json.field "description", html_to_content(self.description_html)
-      json.field "descriptionHtml", self.description_html
-
-      json.field "viewCount", self.views
-      json.field "published", self.published.to_unix
-      json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
-      json.field "lengthSeconds", self.length_seconds
-      json.field "liveNow", self.live_now
-      json.field "paid", self.paid
-      json.field "premium", self.premium
-      json.field "isUpcoming", self.is_upcoming
-
-      if self.premiere_timestamp
-        json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
-      end
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder | Nil = nil)
-    if json
-      to_json(locale, json)
-    else
-      JSON.build do |json|
-        to_json(locale, json)
-      end
-    end
-  end
-
-  def is_upcoming
-    premiere_timestamp ? true : false
-  end
-end
-
-struct SearchPlaylistVideo
-  include DB::Serializable
-
-  property title : String
-  property id : String
-  property length_seconds : Int32
-end
-
-struct SearchPlaylist
-  include DB::Serializable
-
-  property title : String
-  property id : String
-  property author : String
-  property ucid : String
-  property video_count : Int32
-  property videos : Array(SearchPlaylistVideo)
-  property thumbnail : String?
-
-  def to_json(locale, json : JSON::Builder)
-    json.object do
-      json.field "type", "playlist"
-      json.field "title", self.title
-      json.field "playlistId", self.id
-      json.field "playlistThumbnail", self.thumbnail
-
-      json.field "author", self.author
-      json.field "authorId", self.ucid
-      json.field "authorUrl", "/channel/#{self.ucid}"
-
-      json.field "videoCount", self.video_count
-      json.field "videos" do
-        json.array do
-          self.videos.each do |video|
-            json.object do
-              json.field "title", video.title
-              json.field "videoId", video.id
-              json.field "lengthSeconds", video.length_seconds
-
-              json.field "videoThumbnails" do
-                generate_thumbnails(json, video.id)
-              end
-            end
-          end
-        end
-      end
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder | Nil = nil)
-    if json
-      to_json(locale, json)
-    else
-      JSON.build do |json|
-        to_json(locale, json)
-      end
-    end
-  end
-end
-
-struct SearchChannel
-  include DB::Serializable
-
-  property author : String
-  property ucid : String
-  property author_thumbnail : String
-  property subscriber_count : Int32
-  property video_count : Int32
-  property description_html : String
-  property auto_generated : Bool
-
-  def to_json(locale, json : JSON::Builder)
-    json.object do
-      json.field "type", "channel"
-      json.field "author", self.author
-      json.field "authorId", self.ucid
-      json.field "authorUrl", "/channel/#{self.ucid}"
-
-      json.field "authorThumbnails" do
-        json.array do
-          qualities = {32, 48, 76, 100, 176, 512}
-
-          qualities.each do |quality|
-            json.object do
-              json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
-              json.field "width", quality
-              json.field "height", quality
-            end
-          end
-        end
-      end
-
-      json.field "autoGenerated", self.auto_generated
-      json.field "subCount", self.subscriber_count
-      json.field "videoCount", self.video_count
-
-      json.field "description", html_to_content(self.description_html)
-      json.field "descriptionHtml", self.description_html
-    end
-  end
-
-  def to_json(locale, json : JSON::Builder | Nil = nil)
-    if json
-      to_json(locale, json)
-    else
-      JSON.build do |json|
-        to_json(locale, json)
-      end
-    end
-  end
-end
-
-alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist
-
 def channel_search(query, page, channel)
   response = YT_POOL.client &.get("/channel/#{channel}")
 
diff --git a/src/invidious/views/channel/featured_channels.ecr b/src/invidious/views/channel/featured_channels.ecr
index 118fb48d..e2e49d24 100644
--- a/src/invidious/views/channel/featured_channels.ecr
+++ b/src/invidious/views/channel/featured_channels.ecr
@@ -14,7 +14,7 @@
                 <details open="">
                     <summary style="display: revert;">
                         <h3 class="category-heading">
-                            <% if (category_request_param = category.browse_endpoint_param).is_a?(String) %>
+                            <% if (category_request_param = category.browse_endpoint_data).is_a?(String) %>
                                 <a href="/channel/<%=channel.ucid%>/channels/<%=HTML.escape(category_request_param)%>">
                                     <%= category.title %>
                                 </a>
@@ -25,8 +25,12 @@
                     </summary>
                     <% contents = category.contents%>
                     <div class="pure-g section-contents">
-                        <% if contents.is_a?(Array(FeaturedChannel)) %>
+                        <% if contents.is_a?(Array) %>
                             <% contents.each do |item|%>
+                                <% if !item.is_a?(SearchChannel)%>
+                                    <% next %>
+                                <% end %>
+
                                 <div class="channel-profile pure-u-1 pure-u-sm-1-2 pure-u-md-1-3 pure-u-lg-1-4 pure-u-xl-1-5">
                                     <a class="featured-channel-icon" href="/channel/<%= item.ucid %>">
                                         <% if !env.get("preferences").as(Preferences).thin_mode %>
@@ -47,7 +51,11 @@
                                     </div>
                                 </div>
                             <%end%>
-                        <% elsif contents.is_a?(FeaturedChannel) %>
+                        <% elsif contents.is_a?(SearchItem) %>
+                            <% if !contents.is_a?(SearchChannel)%>
+                                <% next %>
+                            <% end %>
+
                             <%item = contents %>
                             <div class="channel-profile large-featured-channel pure-u-1">
                                 <a class="featured-channel-icon" href="/channel/<%= item.ucid %>">
@@ -69,11 +77,10 @@
                                     <% sub_count_text = number_to_short_text(contents.subscriber_count) %>
                                     <%= rendered "components/subscribe_widget" %>
                             </div>
-                        <%end%>
+                        <% end %>
                     </div>
                 </details>
             </div>
-            </div>
         <% end %>
     <% else %>
         <h3 class="pure-u-1">
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 6f027bee..ff486044 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -97,6 +97,7 @@
                     <%= item.responds_to?(:views) && item.views ? translate(locale, "`x` views", number_to_short_text(item.views || 0)) : "" %>
                 </div>
             </h5>
+        <% when Category %>
         <% else %>
             <% if !env.get("preferences").as(Preferences).thin_mode %>
                 <a style="width:100%" href="/watch?v=<%= item.id %>">