From eaac65c97324acd79883697d32c901f11fc8e7ef Mon Sep 17 00:00:00 2001
From: Gabriel Jablonski
Date: Mon, 9 Mar 2026 11:47:41 -0300
Subject: [PATCH] feat: add custom HTML fields for portals (#233)
---
.../api/v1/accounts/portals_controller.rb | 3 +-
.../PortalSettingsPage/PortalBaseSettings.vue | 54 +++++++++++++++++++
.../dashboard/i18n/locale/en/helpCenter.json | 10 ++++
.../i18n/locale/pt_BR/helpCenter.json | 10 ++++
app/models/portal.rb | 4 ++
.../v1/accounts/portals/_portal.json.jbuilder | 2 +
app/views/layouts/portal.html.erb | 6 +++
...260309131532_add_custom_html_to_portals.rb | 6 +++
db/schema.rb | 4 +-
9 files changed, 97 insertions(+), 2 deletions(-)
create mode 100644 db/migrate/20260309131532_add_custom_html_to_portals.rb
diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index b12f89c0a..8800d09ff 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -79,7 +79,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params
params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link,
- :name, :page_title, :slug, :archived, { config: [:default_locale, :show_author, { allowed_locales: [] }] }
+ :name, :page_title, :slug, :archived, :custom_head_html, :custom_body_html,
+ { config: [:default_locale, :show_author, { allowed_locales: [] }] }
)
end
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
index 3258162dc..c39523882 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
@@ -47,6 +47,8 @@ const state = reactive({
logoUrl: '',
avatarBlobId: '',
showAuthor: true,
+ customHeadHtml: '',
+ customBodyHtml: '',
});
const originalState = reactive({ ...state });
@@ -120,6 +122,8 @@ watch(
slug: newVal.slug,
liveChatWidgetInboxId: newVal.inbox?.id || '',
showAuthor: newVal.config?.show_author !== false,
+ customHeadHtml: newVal.custom_head_html || '',
+ customBodyHtml: newVal.custom_body_html || '',
});
if (newVal.logo) {
const {
@@ -153,6 +157,8 @@ const handleUpdatePortal = () => {
blob_id: state.avatarBlobId,
inbox_id: state.liveChatWidgetInboxId,
config: { show_author: state.showAuthor },
+ custom_head_html: state.customHeadHtml,
+ custom_body_html: state.customBodyHtml,
};
emit('updatePortal', portal);
};
@@ -354,6 +360,54 @@ const handleAvatarDelete = () => {
+
+
+
+
+
+ {{
+ t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_HEAD_HTML.HELP_TEXT')
+ }}
+
+
+
+
+
+
+
+
+ {{
+ t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_BODY_HTML.HELP_TEXT')
+ }}
+
+
+
@@ -157,4 +160,7 @@ By default, it renders:
<% if @portal.channel_web_widget.present? && !@is_plain_layout_enabled %>
<%= @portal.channel_web_widget.web_widget_script.html_safe %>
<% end %>
+ <% if !@is_plain_layout_enabled && @portal.custom_body_html.present? %>
+ <%= @portal.custom_body_html.html_safe %>
+ <% end %>
tag of your portal pages"
+ },
"SAVE_CHANGES": "Save changes"
},
"CONFIGURATION_FORM": {
diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json b/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
index cdba5e62d..879bcae2b 100644
--- a/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
+++ b/app/javascript/dashboard/i18n/locale/pt_BR/helpCenter.json
@@ -751,6 +751,16 @@
"LABEL": "Mostrar autores dos artigos",
"HELP_TEXT": "Exibir informações de autoria nas páginas de categorias"
},
+ "CUSTOM_HEAD_HTML": {
+ "LABEL": "HTML personalizado no head",
+ "PLACEHOLDER": "Adicione HTML personalizado na seção
(ex: meta tags, analytics, CSS personalizado)",
+ "HELP_TEXT": "Este código será injetado no
das páginas do seu portal"
+ },
+ "CUSTOM_BODY_HTML": {
+ "LABEL": "HTML personalizado no body",
+ "PLACEHOLDER": "Adicione HTML personalizado antes do (ex: scripts, pixels de rastreamento, widgets)",
+ "HELP_TEXT": "Este código será injetado antes da tag de fechamento das páginas do seu portal"
+ },
"SAVE_CHANGES": "Salvar Alterações"
},
"CONFIGURATION_FORM": {
diff --git a/app/models/portal.rb b/app/models/portal.rb
index 143c880ac..8f3bc655c 100644
--- a/app/models/portal.rb
+++ b/app/models/portal.rb
@@ -6,7 +6,9 @@
# archived :boolean default(FALSE)
# color :string
# config :jsonb
+# custom_body_html :text
# custom_domain :string
+# custom_head_html :text
# header_text :text
# homepage_link :string
# name :string not null
@@ -40,6 +42,8 @@ class Portal < ApplicationRecord
validates :name, presence: true
validates :slug, presence: true, uniqueness: true
validates :custom_domain, uniqueness: true, allow_nil: true
+ validates :custom_head_html, length: { maximum: 15_000 }
+ validates :custom_body_html, length: { maximum: 15_000 }
validate :config_json_format
scope :active, -> { where(archived: false) }
diff --git a/app/views/api/v1/accounts/portals/_portal.json.jbuilder b/app/views/api/v1/accounts/portals/_portal.json.jbuilder
index 4a580e2ca..7628c834c 100644
--- a/app/views/api/v1/accounts/portals/_portal.json.jbuilder
+++ b/app/views/api/v1/accounts/portals/_portal.json.jbuilder
@@ -8,6 +8,8 @@ json.page_title portal.page_title
json.slug portal.slug
json.archived portal.archived
json.account_id portal.account_id
+json.custom_head_html portal.custom_head_html
+json.custom_body_html portal.custom_body_html
json.config do
json.allowed_locales do
diff --git a/app/views/layouts/portal.html.erb b/app/views/layouts/portal.html.erb
index 72b086d4b..cb86c6743 100644
--- a/app/views/layouts/portal.html.erb
+++ b/app/views/layouts/portal.html.erb
@@ -57,6 +57,9 @@ By default, it renders:
document.documentElement.classList.add('light')
}
+ <% if !@is_plain_layout_enabled && @portal.custom_head_html.present? %>
+ <%= @portal.custom_head_html.html_safe %>
+ <% end %>