feat: Add custom domain to article URL if custom domain exists for the portal (#11349)

Portals can have custom domains. When inserting or previewing articles,
we consider the frontend URL. This PR fixes article URL generation to
properly include the portal's custom domain.
This commit is contained in:
Muhsin Keloth
2025-04-22 20:54:09 +05:30
committed by GitHub
parent 9c711bab74
commit e3bacd27d8
5 changed files with 109 additions and 10 deletions

View File

@@ -1,16 +1,55 @@
export const buildPortalURL = portalSlug => {
const { hostURL, helpCenterURL } = window.chatwootConfig;
/**
* Formats a custom domain with https protocol if needed
* @param {string} customDomain - The custom domain to format
* @returns {string} Formatted domain with https protocol
*/
const formatCustomDomain = customDomain =>
customDomain.startsWith('https') ? customDomain : `https://${customDomain}`;
/**
* Gets the default base URL from configuration
* @returns {string} The default base URL
* @throws {Error} If no valid base URL is found
*/
const getDefaultBaseURL = () => {
const { hostURL, helpCenterURL } = window.chatwootConfig || {};
const baseURL = helpCenterURL || hostURL || '';
return `${baseURL}/hc/${portalSlug}`;
if (!baseURL) {
throw new Error('No valid base URL found in configuration');
}
return `${baseURL}/hc`;
};
/**
* Gets the base URL from configuration or custom domain
* @param {string} [customDomain] - Optional custom domain for the portal
* @returns {string} The base URL for the portal
*/
const getPortalBaseURL = customDomain =>
customDomain ? formatCustomDomain(customDomain) : getDefaultBaseURL();
/**
* Builds a portal URL using the provided portal slug and optional custom domain
* @param {string} portalSlug - The slug identifier for the portal
* @param {string} [customDomain] - Optional custom domain for the portal
* @returns {string} The complete portal URL
* @throws {Error} If portalSlug is not provided or invalid
*/
export const buildPortalURL = (portalSlug, customDomain) => {
const baseURL = getPortalBaseURL(customDomain);
return `${baseURL}/${portalSlug}`;
};
export const buildPortalArticleURL = (
portalSlug,
categorySlug,
locale,
articleSlug
articleSlug,
customDomain
) => {
const portalURL = buildPortalURL(portalSlug);
const portalURL = buildPortalURL(portalSlug, customDomain);
return `${portalURL}/articles/${articleSlug}`;
};

View File

@@ -25,5 +25,47 @@ describe('PortalHelper', () => {
).toEqual('https://help.chatwoot.com/hc/handbook/articles/article-slug');
window.chatwootConfig = {};
});
it('returns the correct url with custom domain', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: 'https://help.chatwoot.com',
};
expect(
buildPortalArticleURL(
'handbook',
'culture',
'fr',
'article-slug',
'custom-domain.dev'
)
).toEqual('https://custom-domain.dev/handbook/articles/article-slug');
});
it('handles https in custom domain correctly', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: 'https://help.chatwoot.com',
};
expect(
buildPortalArticleURL(
'handbook',
'culture',
'fr',
'article-slug',
'https://custom-domain.dev'
)
).toEqual('https://custom-domain.dev/handbook/articles/article-slug');
});
it('uses hostURL when helpCenterURL is not available', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: '',
};
expect(
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
).toEqual('https://app.chatwoot.com/hc/handbook/articles/article-slug');
});
});
});

View File

@@ -120,6 +120,7 @@ export default {
mounted() {
this.$store.dispatch('agents/get');
this.$store.dispatch('portals/index');
this.initialize();
this.$watch('$store.state.route', () => this.initialize());
this.$watch('chatList.length', () => {

View File

@@ -1,6 +1,7 @@
<script>
import { debounce } from '@chatwoot/utils';
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex';
import allLocales from 'shared/constants/locales.js';
import SearchHeader from './Header.vue';
@@ -33,6 +34,15 @@ export default {
};
},
computed: {
...mapGetters({
portalBySlug: 'portals/portalBySlug',
}),
portal() {
return this.portalBySlug(this.selectedPortalSlug);
},
portalCustomDomain() {
return this.portal?.custom_domain;
},
articleViewerUrl() {
const article = this.activeArticle(this.activeId);
if (!article) return '';
@@ -47,6 +57,7 @@ export default {
return `${url}`;
},
searchResultsWithUrl() {
return this.searchResults.map(article => ({
...article,
@@ -65,7 +76,8 @@ export default {
this.selectedPortalSlug,
'',
'',
article.slug
article.slug,
this.portalCustomDomain
);
},
localeName(code) {
@@ -111,7 +123,6 @@ export default {
},
onInsert(id) {
const article = this.activeArticle(id || this.activeId);
this.$emit('insert', article);
useAlert(this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED'));
this.onClose();

View File

@@ -20,17 +20,23 @@ const articleById = useMapGetter('articles/articleById');
const article = computed(() => articleById.value(articleSlug));
const portalBySlug = useMapGetter('portals/portalBySlug');
const portal = computed(() => portalBySlug.value(portalSlug));
const isUpdating = ref(false);
const isSaved = ref(false);
const portalLink = computed(() => {
const articleLink = computed(() => {
const { slug: categorySlug, locale: categoryLocale } = article.value.category;
const { slug: articleSlugValue } = article.value;
const portalCustomDomain = portal.value?.custom_domain;
return buildPortalArticleURL(
portalSlug,
categorySlug,
categoryLocale,
articleSlugValue
articleSlugValue,
portalCustomDomain
);
});
@@ -91,7 +97,7 @@ const fetchArticleDetails = () => {
};
const previewArticle = () => {
window.open(portalLink.value, '_blank');
window.open(articleLink.value, '_blank');
useTrack(PORTALS_EVENTS.PREVIEW_ARTICLE, {
status: article.value?.status,
});