feat: add custom HTML fields for portals (#233)

This commit is contained in:
Gabriel Jablonski 2026-03-09 11:47:41 -03:00 committed by GitHub
parent 0fd008f843
commit eaac65c973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 2 deletions

View File

@ -79,7 +79,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link, :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 end

View File

@ -47,6 +47,8 @@ const state = reactive({
logoUrl: '', logoUrl: '',
avatarBlobId: '', avatarBlobId: '',
showAuthor: true, showAuthor: true,
customHeadHtml: '',
customBodyHtml: '',
}); });
const originalState = reactive({ ...state }); const originalState = reactive({ ...state });
@ -120,6 +122,8 @@ watch(
slug: newVal.slug, slug: newVal.slug,
liveChatWidgetInboxId: newVal.inbox?.id || '', liveChatWidgetInboxId: newVal.inbox?.id || '',
showAuthor: newVal.config?.show_author !== false, showAuthor: newVal.config?.show_author !== false,
customHeadHtml: newVal.custom_head_html || '',
customBodyHtml: newVal.custom_body_html || '',
}); });
if (newVal.logo) { if (newVal.logo) {
const { const {
@ -153,6 +157,8 @@ const handleUpdatePortal = () => {
blob_id: state.avatarBlobId, blob_id: state.avatarBlobId,
inbox_id: state.liveChatWidgetInboxId, inbox_id: state.liveChatWidgetInboxId,
config: { show_author: state.showAuthor }, config: { show_author: state.showAuthor },
custom_head_html: state.customHeadHtml,
custom_body_html: state.customBodyHtml,
}; };
emit('updatePortal', portal); emit('updatePortal', portal);
}; };
@ -354,6 +360,54 @@ const handleAvatarDelete = () => {
</span> </span>
</div> </div>
</div> </div>
<div
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
>
<label
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
>
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_HEAD_HTML.LABEL') }}
</label>
<div class="flex flex-col gap-1">
<textarea
v-model="state.customHeadHtml"
:placeholder="
t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_HEAD_HTML.PLACEHOLDER')
"
rows="4"
class="w-full px-3 py-2 text-sm border rounded-lg resize-y border-n-weak bg-transparent dark:bg-transparent text-n-slate-12 placeholder:text-n-slate-9 focus:outline-none focus:ring-1 focus:ring-n-brand font-mono"
/>
<span class="text-xs text-n-slate-11">
{{
t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_HEAD_HTML.HELP_TEXT')
}}
</span>
</div>
</div>
<div
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
>
<label
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
>
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_BODY_HTML.LABEL') }}
</label>
<div class="flex flex-col gap-1">
<textarea
v-model="state.customBodyHtml"
:placeholder="
t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_BODY_HTML.PLACEHOLDER')
"
rows="4"
class="w-full px-3 py-2 text-sm border rounded-lg resize-y border-n-weak bg-transparent dark:bg-transparent text-n-slate-12 placeholder:text-n-slate-9 focus:outline-none focus:ring-1 focus:ring-n-brand font-mono"
/>
<span class="text-xs text-n-slate-11">
{{
t('HELP_CENTER.PORTAL_SETTINGS.FORM.CUSTOM_BODY_HTML.HELP_TEXT')
}}
</span>
</div>
</div>
<div class="flex justify-end w-full gap-2"> <div class="flex justify-end w-full gap-2">
<Button <Button
:label="t('HELP_CENTER.PORTAL_SETTINGS.FORM.SAVE_CHANGES')" :label="t('HELP_CENTER.PORTAL_SETTINGS.FORM.SAVE_CHANGES')"

View File

@ -751,6 +751,16 @@
"LABEL": "Show article authors", "LABEL": "Show article authors",
"HELP_TEXT": "Display author information on category pages" "HELP_TEXT": "Display author information on category pages"
}, },
"CUSTOM_HEAD_HTML": {
"LABEL": "Custom head HTML",
"PLACEHOLDER": "Add custom HTML to the <head> section (e.g., meta tags, analytics, custom CSS)",
"HELP_TEXT": "This code will be injected into the <head> of your portal pages"
},
"CUSTOM_BODY_HTML": {
"LABEL": "Custom body HTML",
"PLACEHOLDER": "Add custom HTML before </body> (e.g., scripts, tracking pixels, widgets)",
"HELP_TEXT": "This code will be injected before the closing </body> tag of your portal pages"
},
"SAVE_CHANGES": "Save changes" "SAVE_CHANGES": "Save changes"
}, },
"CONFIGURATION_FORM": { "CONFIGURATION_FORM": {

View File

@ -751,6 +751,16 @@
"LABEL": "Mostrar autores dos artigos", "LABEL": "Mostrar autores dos artigos",
"HELP_TEXT": "Exibir informações de autoria nas páginas de categorias" "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 <head> (ex: meta tags, analytics, CSS personalizado)",
"HELP_TEXT": "Este código será injetado no <head> das páginas do seu portal"
},
"CUSTOM_BODY_HTML": {
"LABEL": "HTML personalizado no body",
"PLACEHOLDER": "Adicione HTML personalizado antes do </body> (ex: scripts, pixels de rastreamento, widgets)",
"HELP_TEXT": "Este código será injetado antes da tag de fechamento </body> das páginas do seu portal"
},
"SAVE_CHANGES": "Salvar Alterações" "SAVE_CHANGES": "Salvar Alterações"
}, },
"CONFIGURATION_FORM": { "CONFIGURATION_FORM": {

View File

@ -6,7 +6,9 @@
# archived :boolean default(FALSE) # archived :boolean default(FALSE)
# color :string # color :string
# config :jsonb # config :jsonb
# custom_body_html :text
# custom_domain :string # custom_domain :string
# custom_head_html :text
# header_text :text # header_text :text
# homepage_link :string # homepage_link :string
# name :string not null # name :string not null
@ -40,6 +42,8 @@ class Portal < ApplicationRecord
validates :name, presence: true validates :name, presence: true
validates :slug, presence: true, uniqueness: true validates :slug, presence: true, uniqueness: true
validates :custom_domain, uniqueness: true, allow_nil: 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 validate :config_json_format
scope :active, -> { where(archived: false) } scope :active, -> { where(archived: false) }

View File

@ -8,6 +8,8 @@ json.page_title portal.page_title
json.slug portal.slug json.slug portal.slug
json.archived portal.archived json.archived portal.archived
json.account_id portal.account_id 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.config do
json.allowed_locales do json.allowed_locales do

View File

@ -57,6 +57,9 @@ By default, it renders:
document.documentElement.classList.add('light') document.documentElement.classList.add('light')
} }
</script> </script>
<% if !@is_plain_layout_enabled && @portal.custom_head_html.present? %>
<%= @portal.custom_head_html.html_safe %>
<% end %>
</head> </head>
<body> <body>
<div id="portal" class="antialiased"> <div id="portal" class="antialiased">
@ -157,4 +160,7 @@ By default, it renders:
<% if @portal.channel_web_widget.present? && !@is_plain_layout_enabled %> <% if @portal.channel_web_widget.present? && !@is_plain_layout_enabled %>
<%= @portal.channel_web_widget.web_widget_script.html_safe %> <%= @portal.channel_web_widget.web_widget_script.html_safe %>
<% end %> <% end %>
<% if !@is_plain_layout_enabled && @portal.custom_body_html.present? %>
<%= @portal.custom_body_html.html_safe %>
<% end %>
</html> </html>

View File

@ -0,0 +1,6 @@
class AddCustomHtmlToPortals < ActiveRecord::Migration[7.1]
def change
add_column :portals, :custom_head_html, :text
add_column :portals, :custom_body_html, :text
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2026_02_26_194714) do ActiveRecord::Schema[7.1].define(version: 2026_03_09_131532) do
# These extensions should be enabled to support this database # These extensions should be enabled to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "pg_trgm" enable_extension "pg_trgm"
@ -1095,6 +1095,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_02_26_194714) do
t.boolean "archived", default: false t.boolean "archived", default: false
t.bigint "channel_web_widget_id" t.bigint "channel_web_widget_id"
t.jsonb "ssl_settings", default: {}, null: false t.jsonb "ssl_settings", default: {}, null: false
t.text "custom_head_html"
t.text "custom_body_html"
t.index ["channel_web_widget_id"], name: "index_portals_on_channel_web_widget_id" t.index ["channel_web_widget_id"], name: "index_portals_on_channel_web_widget_id"
t.index ["custom_domain"], name: "index_portals_on_custom_domain", unique: true t.index ["custom_domain"], name: "index_portals_on_custom_domain", unique: true
t.index ["slug"], name: "index_portals_on_slug", unique: true t.index ["slug"], name: "index_portals_on_slug", unique: true