diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
index 42d6fa2604..0712d07dd7 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
@@ -1,6 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
-import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
+import { RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
+import { isSocialLinkType } from '~/utils/isSocialLinkType';
import { getSafeUrl } from 'twenty-shared/utils';
type LinkDisplayProps = {
@@ -22,11 +23,7 @@ export const LinkDisplay = ({ value }: LinkDisplayProps) => {
const type = checkUrlType(absoluteUrl);
- if (
- type === LinkType.LinkedIn ||
- type === LinkType.Twitter ||
- type === LinkType.Facebook
- ) {
+ if (isSocialLinkType(type)) {
return ;
}
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx
index 828fee4750..33a7d47d8e 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx
@@ -8,8 +8,9 @@ import {
getUrlHostnameOrThrow,
isDefined,
} from 'twenty-shared/utils';
-import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
+import { RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
+import { isSocialLinkType } from '~/utils/isSocialLinkType';
type LinksDisplayProps = {
value?: FieldLinksValue;
@@ -43,9 +44,7 @@ export const LinksDisplay = ({ value, onLinkClick }: LinksDisplayProps) => {
return (
{links.map(({ url, label, type }, index) =>
- type === LinkType.LinkedIn ||
- type === LinkType.Twitter ||
- type === LinkType.Facebook ? (
+ isSocialLinkType(type) ? (
{
const type = checkUrlType(absoluteUrl);
- if (
- type === LinkType.LinkedIn ||
- type === LinkType.Twitter ||
- type === LinkType.Facebook
- ) {
+ if (isSocialLinkType(type)) {
return (
{
const links = canvas.queryAllByRole('link');
- expect(links).toHaveLength(2);
+ expect(links).toHaveLength(3);
});
const linkedinLink = await canvas.findByText('twenty');
@@ -152,6 +156,44 @@ export const SocialMediaLinks: Story = {
'href',
'https://twitter.com/twentycrm',
);
+
+ const instagramLink = await canvas.findByText('@twenty_hq');
+ expect(instagramLink).toBeVisible();
+ expect(instagramLink).toHaveAttribute(
+ 'href',
+ 'https://www.instagram.com/twenty_hq',
+ );
+ },
+};
+
+export const InstagramLinks: Story = {
+ args: {
+ value: {
+ primaryLinkUrl: 'https://www.instagram.com/twenty_hq',
+ primaryLinkLabel: 'Twenty on Instagram',
+ secondaryLinks: [
+ { url: 'https://instagram.com/p/ABC123', label: 'A post' },
+ { url: 'https://instagram.com/reel/XYZ789', label: 'A reel' },
+ ],
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await waitFor(() => {
+ const links = canvas.queryAllByRole('link');
+ expect(links).toHaveLength(3);
+ });
+
+ const handleLink = await canvas.findByText('@twenty_hq');
+ expect(handleLink).toBeVisible();
+ expect(handleLink).toHaveAttribute(
+ 'href',
+ 'https://www.instagram.com/twenty_hq',
+ );
+
+ const fallbackLinks = await canvas.findAllByText('Instagram');
+ expect(fallbackLinks).toHaveLength(2);
},
};
diff --git a/packages/twenty-front/src/utils/__tests__/checkUrlType.test.ts b/packages/twenty-front/src/utils/__tests__/checkUrlType.test.ts
index 6533fb08c4..c99c3d1808 100644
--- a/packages/twenty-front/src/utils/__tests__/checkUrlType.test.ts
+++ b/packages/twenty-front/src/utils/__tests__/checkUrlType.test.ts
@@ -23,8 +23,16 @@ describe('checkUrlType', () => {
);
});
+ it('should detect Instagram urls', () => {
+ expect(checkUrlType('https://www.instagram.com/ptcrash')).toBe(
+ LinkType.Instagram,
+ );
+ expect(checkUrlType('instagram.com/ptcrash')).toBe(LinkType.Instagram);
+ });
+
it('should fall back to a generic url type', () => {
expect(checkUrlType('https://example.com')).toBe(LinkType.Url);
expect(checkUrlType('not-a-url')).toBe(LinkType.Url);
+ expect(checkUrlType('https://instagram.com')).toBe(LinkType.Url);
});
});
diff --git a/packages/twenty-front/src/utils/__tests__/isSocialLinkType.test.ts b/packages/twenty-front/src/utils/__tests__/isSocialLinkType.test.ts
new file mode 100644
index 0000000000..1a91115913
--- /dev/null
+++ b/packages/twenty-front/src/utils/__tests__/isSocialLinkType.test.ts
@@ -0,0 +1,15 @@
+import { isSocialLinkType } from '~/utils/isSocialLinkType';
+import { LinkType } from 'twenty-ui/navigation';
+
+describe('isSocialLinkType', () => {
+ it('should return true for social link types', () => {
+ expect(isSocialLinkType(LinkType.LinkedIn)).toBe(true);
+ expect(isSocialLinkType(LinkType.Twitter)).toBe(true);
+ expect(isSocialLinkType(LinkType.Facebook)).toBe(true);
+ expect(isSocialLinkType(LinkType.Instagram)).toBe(true);
+ });
+
+ it('should return false for a generic url type', () => {
+ expect(isSocialLinkType(LinkType.Url)).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/utils/checkUrlType.ts b/packages/twenty-front/src/utils/checkUrlType.ts
index f156b33a15..b5ff2612dd 100644
--- a/packages/twenty-front/src/utils/checkUrlType.ts
+++ b/packages/twenty-front/src/utils/checkUrlType.ts
@@ -12,6 +12,9 @@ export const checkUrlType = (url: string) => {
if (/^(https?:\/\/)?(www\.)?facebook\.com\/.+$/.test(url)) {
return LinkType.Facebook;
}
+ if (/^(https?:\/\/)?(www\.)?instagram\.com\/.+$/.test(url)) {
+ return LinkType.Instagram;
+ }
return LinkType.Url;
};
diff --git a/packages/twenty-front/src/utils/isSocialLinkType.ts b/packages/twenty-front/src/utils/isSocialLinkType.ts
new file mode 100644
index 0000000000..f65d3dd08b
--- /dev/null
+++ b/packages/twenty-front/src/utils/isSocialLinkType.ts
@@ -0,0 +1,12 @@
+import { LinkType } from 'twenty-ui/navigation';
+
+// Link types rendered as a social handle (via SocialLink) instead of a plain RoundedLink
+const SOCIAL_LINK_TYPES = [
+ LinkType.LinkedIn,
+ LinkType.Twitter,
+ LinkType.Facebook,
+ LinkType.Instagram,
+];
+
+export const isSocialLinkType = (type: LinkType): boolean =>
+ SOCIAL_LINK_TYPES.includes(type);
diff --git a/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx b/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx
index 2deea3b8e6..8d640e1224 100644
--- a/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx
+++ b/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx
@@ -8,6 +8,7 @@ export enum LinkType {
LinkedIn = 'linkedin',
Twitter = 'twitter',
Facebook = 'facebook',
+ Instagram = 'instagram',
}
type SocialLinkProps = {
diff --git a/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts b/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts
index b3ebc83ffb..fbac84805e 100644
--- a/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts
+++ b/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts
@@ -137,6 +137,80 @@ describe('getDisplayValueByUrlType', () => {
});
});
+ describe('instagram', () => {
+ it('should extract handle from Instagram profile URL with @ prefix', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://www.instagram.com/ptcrash',
+ });
+ expect(result).toBe('@ptcrash');
+ });
+
+ it('should handle Instagram URL without protocol', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'instagram.com/ptcrash',
+ });
+ expect(result).toBe('@ptcrash');
+ });
+
+ it('should handle Instagram URL without www', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/ptcrash',
+ });
+ expect(result).toBe('@ptcrash');
+ });
+
+ it('should ignore a trailing slash', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/ptcrash/',
+ });
+ expect(result).toBe('@ptcrash');
+ });
+
+ it('should ignore a query string', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/ptcrash?hl=en',
+ });
+ expect(result).toBe('@ptcrash');
+ });
+
+ it('should decode URL-encoded characters in the handle', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/john%20doe',
+ });
+ expect(result).toBe('@john doe');
+ });
+
+ it('should return "Instagram" for a post URL', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/p/ABC123',
+ });
+ expect(result).toBe('Instagram');
+ });
+
+ it('should return "Instagram" for a reel URL', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://instagram.com/reel/xyz',
+ });
+ expect(result).toBe('Instagram');
+ });
+
+ it('should return "Instagram" when no handle can be extracted', () => {
+ const result = getDisplayValueByUrlType({
+ type: LinkType.Instagram,
+ href: 'https://www.instagram.com/',
+ });
+ expect(result).toBe('Instagram');
+ });
+ });
+
describe('url type', () => {
it('should return undefined for generic url type', () => {
const result = getDisplayValueByUrlType({
diff --git a/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts
index fa704d0a24..8e8149efbf 100644
--- a/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts
+++ b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts
@@ -1,6 +1,18 @@
import { type LinkType } from '@ui/navigation';
import { isDefined } from '@ui/utilities/utils/isDefined';
+// Instagram path segments that are app routes, not user handles
+const INSTAGRAM_RESERVED_PATHS = [
+ 'p',
+ 'reel',
+ 'reels',
+ 'explore',
+ 'stories',
+ 'tv',
+ 'accounts',
+ 'about',
+];
+
type getUrlDisplayValueByUrlTypeProps = {
type: LinkType;
href: string;
@@ -40,4 +52,19 @@ export const getDisplayValueByUrlType = ({
return 'Facebook';
}
}
+
+ if (type === 'instagram') {
+ const matches = href.match(
+ /(?:https?:\/\/)?(?:www\.)?instagram\.com\/([^/?#]+)/,
+ );
+ const handle = matches?.[1];
+ if (
+ isDefined(handle) &&
+ !INSTAGRAM_RESERVED_PATHS.includes(handle.toLowerCase())
+ ) {
+ return `@${decodeURIComponent(handle)}`;
+ } else {
+ return 'Instagram';
+ }
+ }
};