feat(help-center): add more analytics integrations to portal settings
Adds Google Analytics 4, Hotjar, Plausible, Amplitude, Microsoft Clarity, and Meta Pixel alongside Google Tag Manager. Provider IDs are stored in portal config, validated against safe formats, and rendered as official embed snippets on the public help center.
This commit is contained in:
@@ -79,7 +79,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
params.require(:portal).permit(
|
||||
:id, :color, :custom_domain, :header_text, :homepage_link,
|
||||
:name, :page_title, :slug, :archived,
|
||||
{ config: [:default_locale, :layout, :gtm_container_id, { allowed_locales: [] }, { draft_locales: [] },
|
||||
{ config: [:default_locale, :layout, *Portal::ANALYTICS_CONFIG_FORMATS.keys.map(&:to_sym),
|
||||
{ allowed_locales: [] }, { draft_locales: [] },
|
||||
{ social_profiles: %i[facebook x instagram linkedin youtube tiktok github whatsapp] }] }
|
||||
)
|
||||
end
|
||||
|
||||
+92
-22
@@ -20,8 +20,51 @@ const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
// Mirrors Portal::GTM_CONTAINER_ID_FORMAT on the backend.
|
||||
const GTM_CONTAINER_ID_FORMAT = /^GTM-[A-Z0-9]+$/;
|
||||
// Mirrors Portal::ANALYTICS_CONFIG_FORMATS on the backend.
|
||||
const ANALYTICS_PROVIDERS = [
|
||||
{
|
||||
key: 'gtm_container_id',
|
||||
i18nKey: 'GTM',
|
||||
icon: 'i-logos-google-tag-manager',
|
||||
format: /^GTM-[A-Z0-9]+$/,
|
||||
},
|
||||
{
|
||||
key: 'ga4_measurement_id',
|
||||
i18nKey: 'GA4',
|
||||
icon: 'i-logos-google-analytics',
|
||||
format: /^G-[A-Z0-9]+$/,
|
||||
},
|
||||
{
|
||||
key: 'hotjar_site_id',
|
||||
i18nKey: 'HOTJAR',
|
||||
icon: 'i-logos-hotjar-icon',
|
||||
format: /^\d+$/,
|
||||
},
|
||||
{
|
||||
key: 'plausible_domain',
|
||||
i18nKey: 'PLAUSIBLE',
|
||||
icon: 'i-woot-plausible',
|
||||
format: /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i,
|
||||
},
|
||||
{
|
||||
key: 'amplitude_api_key',
|
||||
i18nKey: 'AMPLITUDE',
|
||||
icon: 'i-logos-amplitude-icon',
|
||||
format: /^[a-z0-9]+$/i,
|
||||
},
|
||||
{
|
||||
key: 'clarity_project_id',
|
||||
i18nKey: 'CLARITY',
|
||||
icon: 'i-woot-microsoft-clarity',
|
||||
format: /^[a-z0-9]+$/i,
|
||||
},
|
||||
{
|
||||
key: 'meta_pixel_id',
|
||||
i18nKey: 'META_PIXEL',
|
||||
icon: 'i-logos-meta-icon',
|
||||
format: /^\d+$/,
|
||||
},
|
||||
];
|
||||
|
||||
const portalConfig = computed(() => props.activePortal?.config || {});
|
||||
|
||||
@@ -43,13 +86,15 @@ const liveChatWidgets = computed(() => {
|
||||
|
||||
const state = reactive({
|
||||
liveChatWidgetInboxId: '',
|
||||
gtmContainerId: '',
|
||||
...Object.fromEntries(ANALYTICS_PROVIDERS.map(({ key }) => [key, ''])),
|
||||
});
|
||||
const originalState = reactive({ ...state });
|
||||
|
||||
const resetFromPortal = () => {
|
||||
state.liveChatWidgetInboxId = props.activePortal?.inbox?.id || '';
|
||||
state.gtmContainerId = portalConfig.value.gtm_container_id || '';
|
||||
ANALYTICS_PROVIDERS.forEach(({ key }) => {
|
||||
state[key] = portalConfig.value[key] || '';
|
||||
});
|
||||
Object.assign(originalState, state);
|
||||
};
|
||||
|
||||
@@ -64,18 +109,27 @@ const liveChatTitle = computed(() =>
|
||||
)
|
||||
);
|
||||
|
||||
const trimmedGtmId = computed(() => state.gtmContainerId.trim());
|
||||
|
||||
const isGtmInvalid = computed(
|
||||
() =>
|
||||
trimmedGtmId.value !== '' &&
|
||||
!GTM_CONTAINER_ID_FORMAT.test(trimmedGtmId.value)
|
||||
const trimmedAnalyticsValues = computed(() =>
|
||||
Object.fromEntries(
|
||||
ANALYTICS_PROVIDERS.map(({ key }) => [key, state[key].trim()])
|
||||
)
|
||||
);
|
||||
|
||||
const invalidAnalyticsKeys = computed(() =>
|
||||
ANALYTICS_PROVIDERS.filter(({ key, format }) => {
|
||||
const value = trimmedAnalyticsValues.value[key];
|
||||
return value !== '' && !format.test(value);
|
||||
}).map(({ key }) => key)
|
||||
);
|
||||
|
||||
const isInvalid = key => invalidAnalyticsKeys.value.includes(key);
|
||||
|
||||
const hasChanges = computed(
|
||||
() =>
|
||||
state.liveChatWidgetInboxId !== originalState.liveChatWidgetInboxId ||
|
||||
trimmedGtmId.value !== originalState.gtmContainerId
|
||||
ANALYTICS_PROVIDERS.some(
|
||||
({ key }) => trimmedAnalyticsValues.value[key] !== originalState[key]
|
||||
)
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -83,7 +137,7 @@ const handleSave = () => {
|
||||
id: props.activePortal.id,
|
||||
slug: props.activePortal.slug,
|
||||
inbox_id: state.liveChatWidgetInboxId,
|
||||
config: { gtm_container_id: trimmedGtmId.value },
|
||||
config: { ...trimmedAnalyticsValues.value },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -132,41 +186,57 @@ const handleSave = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="provider in ANALYTICS_PROVIDERS"
|
||||
:key="provider.key"
|
||||
class="flex flex-col gap-4 p-4 rounded-xl outline outline-1 outline-n-weak"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg size-9 shrink-0 bg-n-alpha-2 text-n-slate-12"
|
||||
>
|
||||
<Icon icon="i-ri-google-fill" class="size-5" />
|
||||
<Icon :icon="provider.icon" class="size-5" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h6 class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.TITLE') }}
|
||||
{{
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.TITLE`
|
||||
)
|
||||
}}
|
||||
</h6>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.DESCRIPTION') }}
|
||||
{{
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.DESCRIPTION`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
v-model="state.gtmContainerId"
|
||||
v-model="state[provider.key]"
|
||||
:placeholder="
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.PLACEHOLDER')
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.PLACEHOLDER`
|
||||
)
|
||||
"
|
||||
:message="
|
||||
isGtmInvalid
|
||||
? t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.INVALID')
|
||||
: t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.HELP')
|
||||
isInvalid(provider.key)
|
||||
? t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.INVALID`
|
||||
)
|
||||
: t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.HELP`
|
||||
)
|
||||
"
|
||||
:message-type="isGtmInvalid ? 'error' : 'info'"
|
||||
:message-type="isInvalid(provider.key) ? 'error' : 'info'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
:label="t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.SAVE')"
|
||||
:disabled="!hasChanges || isGtmInvalid || isFetching"
|
||||
:disabled="!hasChanges || invalidAnalyticsKeys.length > 0 || isFetching"
|
||||
@click="handleSave"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -899,11 +899,53 @@
|
||||
},
|
||||
"GTM": {
|
||||
"TITLE": "Google Tag Manager",
|
||||
"DESCRIPTION": "Add analytics and marketing tags, like Google Analytics, by connecting your Tag Manager container.",
|
||||
"DESCRIPTION": "Add analytics and marketing tags by connecting your Tag Manager container.",
|
||||
"PLACEHOLDER": "GTM-XXXXXXX",
|
||||
"HELP": "Find this in your Google Tag Manager workspace. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid container ID, for example GTM-XXXXXXX."
|
||||
},
|
||||
"GA4": {
|
||||
"TITLE": "Google Analytics 4",
|
||||
"DESCRIPTION": "Measure traffic and visitor behavior on your help center with Google Analytics.",
|
||||
"PLACEHOLDER": "G-XXXXXXXXXX",
|
||||
"HELP": "Find the measurement ID under Admin → Data streams in Google Analytics. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid measurement ID, for example G-XXXXXXXXXX."
|
||||
},
|
||||
"HOTJAR": {
|
||||
"TITLE": "Hotjar",
|
||||
"DESCRIPTION": "Understand how visitors use your help center with heatmaps and session recordings.",
|
||||
"PLACEHOLDER": "1234567",
|
||||
"HELP": "Find your site ID under Sites & organizations in Hotjar. Leave empty to disable.",
|
||||
"INVALID": "Enter a numeric Hotjar site ID."
|
||||
},
|
||||
"PLAUSIBLE": {
|
||||
"TITLE": "Plausible",
|
||||
"DESCRIPTION": "Track help center traffic with privacy-friendly Plausible Analytics.",
|
||||
"PLACEHOLDER": "example.com",
|
||||
"HELP": "Enter the domain configured in your Plausible site settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid domain, for example example.com."
|
||||
},
|
||||
"AMPLITUDE": {
|
||||
"TITLE": "Amplitude",
|
||||
"DESCRIPTION": "Analyze visitor behavior on your help center with Amplitude.",
|
||||
"PLACEHOLDER": "0123456789abcdef0123456789abcdef",
|
||||
"HELP": "Find the API key in your Amplitude project settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid Amplitude API key."
|
||||
},
|
||||
"CLARITY": {
|
||||
"TITLE": "Microsoft Clarity",
|
||||
"DESCRIPTION": "Capture heatmaps and session recordings with Microsoft Clarity.",
|
||||
"PLACEHOLDER": "abcd1234ef",
|
||||
"HELP": "Find the project ID in your Clarity project settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid Clarity project ID."
|
||||
},
|
||||
"META_PIXEL": {
|
||||
"TITLE": "Meta Pixel",
|
||||
"DESCRIPTION": "Measure ad conversions and build audiences from help center visits with Meta Pixel.",
|
||||
"PLACEHOLDER": "123456789012345",
|
||||
"HELP": "Find the pixel ID in Meta Events Manager. Leave empty to disable.",
|
||||
"INVALID": "Enter a numeric pixel ID."
|
||||
},
|
||||
"SAVE": "Save changes"
|
||||
},
|
||||
"NAV": {
|
||||
|
||||
+24
-13
@@ -43,15 +43,28 @@ class Portal < ApplicationRecord
|
||||
validates :slug, presence: true, uniqueness: true
|
||||
validates :custom_domain, uniqueness: true, allow_nil: true
|
||||
validate :config_json_format
|
||||
validate :gtm_container_id_format
|
||||
validate :analytics_config_format
|
||||
|
||||
scope :active, -> { where(archived: false) }
|
||||
|
||||
CONFIG_JSON_KEYS = %w[allowed_locales default_locale draft_locales website_token social_profiles layout gtm_container_id].freeze
|
||||
# Analytics values are restricted to shapes safe to interpolate into the help center
|
||||
# markup (no quotes/angle brackets). Mirrored by ANALYTICS_PROVIDERS in the frontend.
|
||||
ANALYTICS_CONFIG_FORMATS = {
|
||||
'gtm_container_id' => /\AGTM-[A-Z0-9]+\z/,
|
||||
'ga4_measurement_id' => /\AG-[A-Z0-9]+\z/,
|
||||
'hotjar_site_id' => /\A\d+\z/,
|
||||
'plausible_domain' => /\A[a-z0-9]([a-z0-9.-]*[a-z0-9])?\z/i,
|
||||
'amplitude_api_key' => /\A[a-z0-9]+\z/i,
|
||||
'clarity_project_id' => /\A[a-z0-9]+\z/i,
|
||||
'meta_pixel_id' => /\A\d+\z/
|
||||
}.freeze
|
||||
|
||||
# Google Tag Manager container IDs look like `GTM-XXXXXX`. Restricting the value to this
|
||||
# shape keeps it safe to interpolate into the help center markup (no quotes/angle brackets).
|
||||
GTM_CONTAINER_ID_FORMAT = /\AGTM-[A-Z0-9]+\z/
|
||||
CONFIG_JSON_KEYS = (%w[allowed_locales default_locale draft_locales website_token social_profiles layout] +
|
||||
ANALYTICS_CONFIG_FORMATS.keys).freeze
|
||||
|
||||
ANALYTICS_CONFIG_FORMATS.each_key do |key|
|
||||
define_method(key) { config_value(key).presence }
|
||||
end
|
||||
|
||||
def file_base_data
|
||||
{
|
||||
@@ -107,17 +120,15 @@ class Portal < ApplicationRecord
|
||||
config_value('social_profiles') || {}
|
||||
end
|
||||
|
||||
def gtm_container_id
|
||||
config_value('gtm_container_id').presence
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gtm_container_id_format
|
||||
value = config_value('gtm_container_id')
|
||||
return if value.blank?
|
||||
def analytics_config_format
|
||||
ANALYTICS_CONFIG_FORMATS.each do |key, format|
|
||||
value = config_value(key)
|
||||
next if value.blank?
|
||||
|
||||
errors.add(:config, 'Google Tag Manager container ID is invalid') unless value.to_s.match?(GTM_CONTAINER_ID_FORMAT)
|
||||
errors.add(:config, "#{key.humanize} is invalid") unless value.to_s.match?(format)
|
||||
end
|
||||
end
|
||||
|
||||
def config_json_format
|
||||
|
||||
@@ -19,6 +19,12 @@ json.config do
|
||||
json.layout portal.layout
|
||||
json.social_profiles portal.social_profiles
|
||||
json.gtm_container_id portal.gtm_container_id
|
||||
json.ga4_measurement_id portal.ga4_measurement_id
|
||||
json.hotjar_site_id portal.hotjar_site_id
|
||||
json.plausible_domain portal.plausible_domain
|
||||
json.amplitude_api_key portal.amplitude_api_key
|
||||
json.clarity_project_id portal.clarity_project_id
|
||||
json.meta_pixel_id portal.meta_pixel_id
|
||||
end
|
||||
|
||||
if portal.channel_web_widget
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%# Google Tag Manager. The container ID is validated against Portal::GTM_CONTAINER_ID_FORMAT, %>
|
||||
<%# so it is restricted to `GTM-` + alphanumerics and is safe to interpolate here. %>
|
||||
<%# Analytics tags. Every value below is validated against Portal::ANALYTICS_CONFIG_FORMATS, %>
|
||||
<%# so it is restricted to a safe shape (no quotes/angle brackets) before interpolation. %>
|
||||
<% if @portal.gtm_container_id.present? %>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
@@ -11,3 +11,66 @@
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<% end %>
|
||||
<% if @portal.ga4_measurement_id.present? %>
|
||||
<!-- Google Analytics 4 -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= @portal.ga4_measurement_id %>"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<%= j @portal.ga4_measurement_id %>');
|
||||
</script>
|
||||
<!-- End Google Analytics 4 -->
|
||||
<% end %>
|
||||
<% if @portal.hotjar_site_id.present? %>
|
||||
<!-- Hotjar -->
|
||||
<script>
|
||||
(function(h,o,t,j,a,r){
|
||||
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
|
||||
h._hjSettings={hjid:<%= @portal.hotjar_site_id.to_i %>,hjsv:6};
|
||||
a=o.getElementsByTagName('head')[0];
|
||||
r=o.createElement('script');r.async=1;
|
||||
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
|
||||
a.appendChild(r);
|
||||
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
|
||||
</script>
|
||||
<!-- End Hotjar -->
|
||||
<% end %>
|
||||
<% if @portal.plausible_domain.present? %>
|
||||
<!-- Plausible -->
|
||||
<script defer data-domain="<%= @portal.plausible_domain %>" src="https://plausible.io/js/script.js"></script>
|
||||
<!-- End Plausible -->
|
||||
<% end %>
|
||||
<% if @portal.amplitude_api_key.present? %>
|
||||
<!-- Amplitude -->
|
||||
<script src="https://cdn.amplitude.com/script/<%= @portal.amplitude_api_key %>.js"></script>
|
||||
<script>window.amplitude.init('<%= j @portal.amplitude_api_key %>');</script>
|
||||
<!-- End Amplitude -->
|
||||
<% end %>
|
||||
<% if @portal.clarity_project_id.present? %>
|
||||
<!-- Microsoft Clarity -->
|
||||
<script>
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "<%= j @portal.clarity_project_id %>");
|
||||
</script>
|
||||
<!-- End Microsoft Clarity -->
|
||||
<% end %>
|
||||
<% if @portal.meta_pixel_id.present? %>
|
||||
<!-- Meta Pixel -->
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '<%= j @portal.meta_pixel_id %>');
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
<!-- End Meta Pixel -->
|
||||
<% end %>
|
||||
|
||||
@@ -4,3 +4,9 @@
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
<% end %>
|
||||
<% if @portal.meta_pixel_id.present? %>
|
||||
<!-- Meta Pixel (noscript) -->
|
||||
<noscript><img height="1" width="1" style="display:none"
|
||||
src="https://www.facebook.com/tr?id=<%= @portal.meta_pixel_id %>&ev=PageView&noscript=1"/></noscript>
|
||||
<!-- End Meta Pixel (noscript) -->
|
||||
<% end %>
|
||||
|
||||
@@ -173,7 +173,14 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
|
||||
],
|
||||
'default_locale' => 'en',
|
||||
'layout' => 'classic',
|
||||
'social_profiles' => {}
|
||||
'social_profiles' => {},
|
||||
'gtm_container_id' => nil,
|
||||
'ga4_measurement_id' => nil,
|
||||
'hotjar_site_id' => nil,
|
||||
'plausible_domain' => nil,
|
||||
'amplitude_api_key' => nil,
|
||||
'clarity_project_id' => nil,
|
||||
'meta_pixel_id' => nil
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -428,5 +428,15 @@ export const icons = {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
plausible: {
|
||||
body: `<linearGradient id="SVGcy95AgrO" x1="189.056" x2="296.848" y1="470.428" y2="659.063" gradientTransform="translate(0 -278.024)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#909cf7"/><stop offset="1" stop-color="#4b38d8"/></linearGradient><path fill="url(#SVGcy95AgrO)" d="M448.6 192.9c-9.3 89.2-87.3 155.5-177 155.5H237v81.7c0 45.2-36.7 81.9-81.9 81.9h-64c-15.8 0-28.7-12.8-28.7-28.7V315.2l43-60.3c7.8-10.9 22.1-15 34.4-9.8l24.5 10.2c12.3 5.2 26.6 1.1 34.3-9.8l57.3-80.4c7.7-10.9 22-14.9 34.3-9.7l47.1 19.8c12.3 5.2 26.6 1.1 34.3-9.8l55.1-77.3c17.4 30.4 25.9 66.5 21.9 104.8"/><linearGradient id="SVGjYVYSdaH" x1="130.554" x2="241.634" y1="266.456" y2="460.846" gradientTransform="translate(0 -278.024)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#909cf7"/><stop offset="1" stop-color="#4b38d8"/></linearGradient><path fill="url(#SVGjYVYSdaH)" d="M90.6 246.4c7-9.9 17.2-17.4 29.1-19.7c9.3-1.8 18.4-.8 26.9 2.7l24.4 10.2c1.4.6 2.9.9 4.4.9c3.7 0 7.2-1.8 9.4-4.8l56.3-78.9c7-9.8 17.2-17.4 29.1-19.7c9.2-1.8 18.3-.8 26.7 2.7l47.1 19.8c1.4.6 2.9.9 4.4.9c3.7 0 7.2-1.8 9.4-4.8l59-82.8C385.3 28.7 333.7 0 275.3 0H91.1C75.3 0 62.5 12.8 62.5 28.7v257.1z"/>`,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
'microsoft-clarity': {
|
||||
body: `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M24 7.69a2.1 2.1 0 0 0-1.823 1.052l-16.4 28.412A2.104 2.104 0 0 0 7.6 40.31h32.8a2.104 2.104 0 0 0 1.823-3.156l-16.4-28.412A2.1 2.1 0 0 0 24 7.689"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m32.656 20.58l-20.349 5.262l29.821 13.565"/>`,
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
/** Ends */
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user