diff --git a/plugins/chat/app/models/chat/message.rb b/plugins/chat/app/models/chat/message.rb
index a236ffa2f7d..2d0c49e688f 100644
--- a/plugins/chat/app/models/chat/message.rb
+++ b/plugins/chat/app/models/chat/message.rb
@@ -8,6 +8,7 @@ module Chat
     self.table_name = "chat_messages"
 
     BAKED_VERSION = 2
+    EXCERPT_LENGTH = 150
 
     attribute :has_oneboxes, default: false
 
@@ -122,7 +123,7 @@ module Chat
       end
     end
 
-    def excerpt(max_length: 100)
+    def build_excerpt
       # just show the URL if the whole message is a URL, because we cannot excerpt oneboxes
       return message if UrlHelper.relaxed_parse(message).is_a?(URI)
 
@@ -130,11 +131,7 @@ module Chat
       return uploads.first.original_filename if cooked.blank? && uploads.present?
 
       # this may return blank for some complex things like quotes, that is acceptable
-      PrettyText.excerpt(cooked, max_length, strip_links: true, keep_mentions: true)
-    end
-
-    def censored_excerpt(max_length: 100)
-      WordWatcher.censor(excerpt(max_length: max_length))
+      PrettyText.excerpt(cooked, EXCERPT_LENGTH, strip_links: true, keep_mentions: true)
     end
 
     def cooked_for_excerpt
diff --git a/plugins/chat/app/models/chat/thread.rb b/plugins/chat/app/models/chat/thread.rb
index 8de438c01e4..e9482796a28 100644
--- a/plugins/chat/app/models/chat/thread.rb
+++ b/plugins/chat/app/models/chat/thread.rb
@@ -2,7 +2,6 @@
 
 module Chat
   class Thread < ActiveRecord::Base
-    EXCERPT_LENGTH = 150
     MAX_TITLE_LENGTH = 100
 
     include Chat::ThreadCache
@@ -68,7 +67,7 @@ module Chat
     end
 
     def excerpt
-      original_message.excerpt(max_length: EXCERPT_LENGTH)
+      original_message.excerpt
     end
 
     def update_last_message_id!
diff --git a/plugins/chat/app/serializers/chat/in_reply_to_serializer.rb b/plugins/chat/app/serializers/chat/in_reply_to_serializer.rb
index 5d69815628f..06a85d65aa6 100644
--- a/plugins/chat/app/serializers/chat/in_reply_to_serializer.rb
+++ b/plugins/chat/app/serializers/chat/in_reply_to_serializer.rb
@@ -7,10 +7,6 @@ module Chat
 
     attributes :id, :cooked, :excerpt
 
-    def excerpt
-      object.censored_excerpt
-    end
-
     def user
       object.user || Chat::NullUser.new
     end
diff --git a/plugins/chat/app/serializers/chat/message_serializer.rb b/plugins/chat/app/serializers/chat/message_serializer.rb
index 5c43a6062f9..11894de7ca5 100644
--- a/plugins/chat/app/serializers/chat/message_serializer.rb
+++ b/plugins/chat/app/serializers/chat/message_serializer.rb
@@ -57,7 +57,7 @@ module Chat
     end
 
     def excerpt
-      object.censored_excerpt
+      object.excerpt || object.build_excerpt
     end
 
     def reactions
diff --git a/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb b/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb
index e686f1059b1..9fda1a0946f 100644
--- a/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb
+++ b/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb
@@ -12,10 +12,6 @@ module Chat
                :mentioned_users,
                :user
 
-    def excerpt
-      object.censored_excerpt
-    end
-
     def mentioned_users
       object
         .user_mentions
diff --git a/plugins/chat/app/serializers/chat/thread_preview_serializer.rb b/plugins/chat/app/serializers/chat/thread_preview_serializer.rb
index e7fb373d11c..65ba953c4e3 100644
--- a/plugins/chat/app/serializers/chat/thread_preview_serializer.rb
+++ b/plugins/chat/app/serializers/chat/thread_preview_serializer.rb
@@ -28,7 +28,7 @@ module Chat
     end
 
     def last_reply_excerpt
-      object.last_message.excerpt(max_length: Chat::Thread::EXCERPT_LENGTH)
+      object.last_message.excerpt
     end
 
     def last_reply_user
diff --git a/plugins/chat/app/services/chat/create_message.rb b/plugins/chat/app/services/chat/create_message.rb
index fa06798c2a9..a943ed6a43c 100644
--- a/plugins/chat/app/services/chat/create_message.rb
+++ b/plugins/chat/app/services/chat/create_message.rb
@@ -37,6 +37,7 @@ module Chat
     model :message_instance, :instantiate_message
 
     transaction do
+      step :create_excerpt
       step :save_message
       step :delete_drafts
       step :post_process_thread
@@ -222,6 +223,10 @@ module Chat
       end
     end
 
+    def create_excerpt(message_instance:)
+      message_instance.excerpt = message_instance.build_excerpt
+    end
+
     def publish_user_tracking_state(message_instance:, channel:, channel_membership:, guardian:)
       message_to_publish = message_instance
       message_to_publish =
diff --git a/plugins/chat/app/services/chat/update_message.rb b/plugins/chat/app/services/chat/update_message.rb
index dae7d27227b..2ea3c6057ea 100644
--- a/plugins/chat/app/services/chat/update_message.rb
+++ b/plugins/chat/app/services/chat/update_message.rb
@@ -24,6 +24,7 @@ module Chat
 
     transaction do
       step :modify_message
+      step :update_excerpt
       step :save_message
       step :save_revision
       step :publish
@@ -95,6 +96,10 @@ module Chat
       message.upload_ids = new_upload_ids
     end
 
+    def update_excerpt(message:)
+      message.excerpt = message.build_excerpt
+    end
+
     def save_message(message:)
       message.save!
     end
diff --git a/plugins/chat/db/migrate/20240422042830_add_excerpt_to_chat_messages.rb b/plugins/chat/db/migrate/20240422042830_add_excerpt_to_chat_messages.rb
new file mode 100644
index 00000000000..e5210d4c27b
--- /dev/null
+++ b/plugins/chat/db/migrate/20240422042830_add_excerpt_to_chat_messages.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddExcerptToChatMessages < ActiveRecord::Migration[7.0]
+  def change
+    add_column :chat_messages, :excerpt, :string, limit: 1000, null: true
+  end
+end
diff --git a/plugins/chat/spec/integration/outgoing_web_hooks_spec.rb b/plugins/chat/spec/integration/outgoing_web_hooks_spec.rb
index 8426b7f5bb9..67c4ed0ca49 100644
--- a/plugins/chat/spec/integration/outgoing_web_hooks_spec.rb
+++ b/plugins/chat/spec/integration/outgoing_web_hooks_spec.rb
@@ -102,20 +102,12 @@ RSpec.describe "Outgoing chat webhooks" do
     context "for a category channel" do
       fab!(:category)
       fab!(:chat_channel) { Fabricate(:category_channel, chatable: category) }
-      fab!(:chat_message) { Fabricate(:chat_message, chat_channel: chat_channel, user: user1) }
-
-      before do
-        [user1, user2].each do |user|
-          Chat::UserChatChannelMembership.create(
-            user: user,
-            chat_channel: chat_channel,
-            following: true,
-          )
-        end
-
-        sign_in(user1)
+      fab!(:chat_message) do
+        Fabricate(:chat_message, use_service: true, chat_channel: chat_channel, user: user1)
       end
 
+      before { sign_in(user1) }
+
       it "triggers a webhook when a chat message is created" do
         post "/chat/#{chat_channel.id}.json", params: { message: message_content }
 
@@ -175,7 +167,12 @@ RSpec.describe "Outgoing chat webhooks" do
       fab!(:direct_message) { Fabricate(:direct_message, users: [user1, user2]) }
       fab!(:direct_message_channel) { Fabricate(:direct_message_channel, chatable: direct_message) }
       fab!(:chat_message) do
-        Fabricate(:chat_message, chat_channel: direct_message_channel, user: user1)
+        Fabricate(
+          :chat_message,
+          use_service: true,
+          chat_channel: direct_message_channel,
+          user: user1,
+        )
       end
 
       before { sign_in(user1) }
diff --git a/plugins/chat/spec/models/chat/message_spec.rb b/plugins/chat/spec/models/chat/message_spec.rb
index 9cf725caab9..cb294315a36 100644
--- a/plugins/chat/spec/models/chat/message_spec.rb
+++ b/plugins/chat/spec/models/chat/message_spec.rb
@@ -277,7 +277,9 @@ describe Chat::Message do
           :chat_message,
           message: "https://twitter.com/EffinBirds/status/1518743508378697729",
         )
-      expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729")
+      expect(message.build_excerpt).to eq(
+        "https://twitter.com/EffinBirds/status/1518743508378697729",
+      )
       message =
         Fabricate.build(
           :chat_message,
@@ -286,7 +288,9 @@ describe Chat::Message do
           <aside class=\"onebox twitterstatus\" data-onebox-src=\"https://twitter.com/EffinBirds/status/1518743508378697729\">\n  <header class=\"source\">\n\n      <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">twitter.com</a>\n  </header>\n\n  <article class=\"onebox-body\">\n    \n<h4><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">Effin' Birds</a></h4>\n<div class=\"twitter-screen-name\"><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">@EffinBirds</a></div>\n\n<div class=\"tweet\">\n  <span class=\"tweet-description\">https://t.co/LjlqMm9lck</span>\n</div>\n\n<div class=\"date\">\n  <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" class=\"timestamp\" target=\"_blank\" rel=\"nofollow ugc noopener\">5:07 PM - 25 Apr 2022</a>\n\n    <span class=\"like\">\n      <svg viewbox=\"0 0 512 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z\"></path>\n      </svg>\n      2.5K\n    </span>\n\n    <span class=\"retweet\">\n      <svg viewbox=\"0 0 640 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M629.657 343.598L528.971 444.284c-9.373 9.372-24.568 9.372-33.941 0L394.343 343.598c-9.373-9.373-9.373-24.569 0-33.941l10.823-10.823c9.562-9.562 25.133-9.34 34.419.492L480 342.118V160H292.451a24.005 24.005 0 0 1-16.971-7.029l-16-16C244.361 121.851 255.069 96 276.451 96H520c13.255 0 24 10.745 24 24v222.118l40.416-42.792c9.285-9.831 24.856-10.054 34.419-.492l10.823 10.823c9.372 9.372 9.372 24.569-.001 33.941zm-265.138 15.431A23.999 23.999 0 0 0 347.548 352H160V169.881l40.416 42.792c9.286 9.831 24.856 10.054 34.419.491l10.822-10.822c9.373-9.373 9.373-24.569 0-33.941L144.971 67.716c-9.373-9.373-24.569-9.373-33.941 0L10.343 168.402c-9.373 9.373-9.373 24.569 0 33.941l10.822 10.822c9.562 9.562 25.133 9.34 34.419-.491L96 169.881V392c0 13.255 10.745 24 24 24h243.549c21.382 0 32.09-25.851 16.971-40.971l-16.001-16z\"></path>\n      </svg>\n      499\n    </span>\n</div>\n\n  </article>\n\n  <div class=\"onebox-metadata\">\n    \n    \n  </div>\n\n  <div style=\"clear: both\"></div>\n</aside>\n
         COOKED
         )
-      expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729")
+      expect(message.build_excerpt).to eq(
+        "https://twitter.com/EffinBirds/status/1518743508378697729",
+      )
     end
 
     it "excerpts upload file name if message is empty" do
@@ -294,7 +298,7 @@ describe Chat::Message do
         Fabricate(:upload, original_filename: "cat.gif", width: 400, height: 300, extension: "gif")
       message = Fabricate(:chat_message, message: "", uploads: [gif])
 
-      expect(message.excerpt).to eq "cat.gif"
+      expect(message.build_excerpt).to eq "cat.gif"
     end
 
     it "supports autolink with <>" do
diff --git a/plugins/chat/spec/plugin_helper.rb b/plugins/chat/spec/plugin_helper.rb
index 8b6451f04b6..2b4fec15ba2 100644
--- a/plugins/chat/spec/plugin_helper.rb
+++ b/plugins/chat/spec/plugin_helper.rb
@@ -55,9 +55,7 @@ module ChatSystemHelpers
   end
 
   def thread_excerpt(message)
-    CGI.escapeHTML(
-      message.censored_excerpt(max_length: ::Chat::Thread::EXCERPT_LENGTH).gsub("&hellip;", "…"),
-    )
+    message.excerpt
   end
 end
 
diff --git a/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb b/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb
index 74e1c84634f..d8e516f7478 100644
--- a/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb
+++ b/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb
@@ -44,7 +44,7 @@ describe Chat::MessageSerializer do
   describe "#excerpt" do
     it "censors words" do
       watched_word = Fabricate(:watched_word, action: WatchedWord.actions[:censor])
-      message = Fabricate(:chat_message, message: "ok #{watched_word.word}")
+      message = Fabricate(:chat_message, use_service: true, message: "ok #{watched_word.word}")
       serializer = described_class.new(message, scope: guardian, root: nil)
 
       expect(serializer.as_json[:excerpt]).to eq("ok ■■■■■")
diff --git a/plugins/chat/spec/serializer/chat/in_reply_to_serializer_spec.rb b/plugins/chat/spec/serializer/chat/in_reply_to_serializer_spec.rb
index a2bb283d7cb..6c2e8c0409a 100644
--- a/plugins/chat/spec/serializer/chat/in_reply_to_serializer_spec.rb
+++ b/plugins/chat/spec/serializer/chat/in_reply_to_serializer_spec.rb
@@ -23,7 +23,9 @@ RSpec.describe Chat::InReplyToSerializer do
 
   describe "#excerpt" do
     let(:watched_word) { Fabricate(:watched_word, action: WatchedWord.actions[:censor]) }
-    let(:message) { Fabricate(:chat_message, message: "ok #{watched_word.word}") }
+    let(:message) do
+      Fabricate(:chat_message, use_service: true, message: "ok #{watched_word.word}")
+    end
 
     it "censors words" do
       expect(serializer.as_json[:excerpt]).to eq("ok ■■■■■")
diff --git a/plugins/chat/spec/services/chat/create_message_spec.rb b/plugins/chat/spec/services/chat/create_message_spec.rb
index 54e9bd7f47e..79b23b52214 100644
--- a/plugins/chat/spec/services/chat/create_message_spec.rb
+++ b/plugins/chat/spec/services/chat/create_message_spec.rb
@@ -57,6 +57,10 @@ RSpec.describe Chat::CreateMessage do
         expect(message).to be_cooked
       end
 
+      it "creates the excerpt" do
+        expect(message).to have_attributes(excerpt: content)
+      end
+
       it "creates mentions" do
         Jobs.run_immediately!
         expect { result }.to change { Chat::Mention.count }.by(1)
diff --git a/plugins/chat/spec/services/chat/update_message_spec.rb b/plugins/chat/spec/services/chat/update_message_spec.rb
index d46bc20aa9c..646f7716e84 100644
--- a/plugins/chat/spec/services/chat/update_message_spec.rb
+++ b/plugins/chat/spec/services/chat/update_message_spec.rb
@@ -145,6 +145,17 @@ RSpec.describe Chat::UpdateMessage do
       expect(chat_message.reload.cooked).to eq("<p>Change <strong>to</strong> this!</p>")
     end
 
+    it "updates the excerpt" do
+      chat_message = create_chat_message(user1, "This is a message", public_chat_channel)
+
+      described_class.call(
+        guardian: guardian,
+        message_id: chat_message.id,
+        message: "Change to this!",
+      )
+      expect(chat_message.reload.excerpt).to eq("Change to this!")
+    end
+
     it "publishes a DiscourseEvent for updated messages" do
       chat_message = create_chat_message(user1, "This will be changed", public_chat_channel)
       events =
@@ -740,6 +751,9 @@ RSpec.describe Chat::UpdateMessage do
 
     describe "watched words" do
       fab!(:watched_word)
+      let!(:censored_word) do
+        Fabricate(:watched_word, word: "test", action: WatchedWord.actions[:censor])
+      end
 
       it "errors when a blocked word is present" do
         chat_message = create_chat_message(user1, "something", public_chat_channel)
@@ -755,6 +769,18 @@ RSpec.describe Chat::UpdateMessage do
 
         expect(chat_message.reload.message).not_to eq("bad word - #{watched_word.word}")
       end
+
+      it "hides censored word within the excerpt" do
+        chat_message = create_chat_message(user1, "something", public_chat_channel)
+
+        described_class.call(
+          guardian: guardian,
+          message_id: chat_message.id,
+          message: "bad word - #{censored_word.word}",
+        )
+
+        expect(chat_message.reload.excerpt).to eq("bad word - ■■■■")
+      end
     end
 
     describe "channel statuses" do
diff --git a/plugins/chat/spec/system/chat/composer/channel_spec.rb b/plugins/chat/spec/system/chat/composer/channel_spec.rb
index 2c18551ec79..fb30596c980 100644
--- a/plugins/chat/spec/system/chat/composer/channel_spec.rb
+++ b/plugins/chat/spec/system/chat/composer/channel_spec.rb
@@ -18,7 +18,12 @@ RSpec.describe "Chat | composer | channel", type: :system do
   describe "reply to message" do
     context "when raw contains html" do
       fab!(:message_1) do
-        Fabricate(:chat_message, chat_channel: channel_1, message: "<mark>not marked</mark>")
+        Fabricate(
+          :chat_message,
+          use_service: true,
+          chat_channel: channel_1,
+          message: "<mark>not marked</mark>",
+        )
       end
 
       it "renders text in the details" do
diff --git a/plugins/chat/spec/system/chat_channel_spec.rb b/plugins/chat/spec/system/chat_channel_spec.rb
index 48960115335..1c97ad6b4ef 100644
--- a/plugins/chat/spec/system/chat_channel_spec.rb
+++ b/plugins/chat/spec/system/chat_channel_spec.rb
@@ -3,7 +3,7 @@
 RSpec.describe "Chat channel", type: :system do
   fab!(:current_user) { Fabricate(:user) }
   fab!(:channel_1) { Fabricate(:chat_channel) }
-  fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
+  fab!(:message_1) { Fabricate(:chat_message, use_service: true, chat_channel: channel_1) }
 
   let(:chat_page) { PageObjects::Pages::Chat.new }
   let(:channel_page) { PageObjects::Pages::ChatChannel.new }
@@ -278,6 +278,7 @@ RSpec.describe "Chat channel", type: :system do
         :chat_message,
         user: other_user,
         chat_channel: channel_1,
+        use_service: true,
         message: "<mark>not marked</mark>",
       )
     end
diff --git a/plugins/chat/spec/system/chat_composer_draft_spec.rb b/plugins/chat/spec/system/chat_composer_draft_spec.rb
index c64aa3dcce0..bd06e069bd5 100644
--- a/plugins/chat/spec/system/chat_composer_draft_spec.rb
+++ b/plugins/chat/spec/system/chat_composer_draft_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe "Chat composer draft", type: :system do
   fab!(:message_1) do
     Fabricate(
       :chat_message,
+      use_service: true,
       chat_channel: channel_1,
       message: "This is a message for draft and replies",
     )