Skip to content

Commit ad45d8d

Browse files
authored
feat: add utm params in external urls (#120)
* feat: add getUrlWithUtmTrackingParams util function * feat: add utm params in all anchor tags * fix: add medium param in getUrlWithUtmTrackingParams * fix: util module exports * feat: add getUrlWithUtmTrackingParams in issueEmailGenerator script
1 parent 4a096ef commit ad45d8d

File tree

8 files changed

+131
-40
lines changed

8 files changed

+131
-40
lines changed

components/ArticleItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import Tags from './common/Tags';
44
import { useThemeState } from '../theme/ThemeContext';
55
import Markdown from './Markdown';
66
import Authors from './Authors';
7+
import { getUrlWithUtmTrackingParams } from '../utils';
78

89
const ArticleItem = ({ article }: { article: Article }): JSX.Element => {
910
const theme = useThemeState();
1011
const { title, desc, url, authors, tags } = article;
1112

13+
const articleUrlWithTrackingParams = getUrlWithUtmTrackingParams({ url });
14+
1215
return (
1316
<div className="mt-0 mx-0 py-4" key={url}>
14-
<a href={url} target="_blank" rel="noreferrer">
17+
<a href={articleUrlWithTrackingParams} target="_blank" rel="noreferrer">
1518
<Text size="2xl" as="h4" color={`text-${theme}-600`} additionalStyles="hover:underline">
1619
{title}
1720
</Text>

components/Authors.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Author from '../interfaces/author';
2+
import { getUrlWithUtmTrackingParams } from '../utils';
23
import Text from './common/Text';
34

45
function Authors({ authors }: { authors: Array<string> | Array<Author> }): JSX.Element {
@@ -19,8 +20,10 @@ function Authors({ authors }: { authors: Array<string> | Array<Author> }): JSX.E
1920
);
2021
}
2122

23+
const authorUrlWithTrackingParams = getUrlWithUtmTrackingParams({ url: author.website });
24+
2225
return (
23-
<a href={author.website} target="_blank" rel="noreferrer" key={author.id}>
26+
<a href={authorUrlWithTrackingParams} target="_blank" rel="noreferrer" key={author.id}>
2427
<Text as="span" additionalStyles="uppercase hover:underline" size="sm" color="text-gray-600">
2528
{isLastElement ? author.name : `${author.name}, `}
2629
</Text>

components/Curators.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useThemeState } from '../theme/ThemeContext';
44
import SocialLinks from './common/SocialLinks';
55
import Text from './common/Text';
66
import Markdown from './Markdown';
7+
import { getUrlWithUtmTrackingParams } from '../utils';
78

89
function shuffle(array: unknown[]): unknown[] {
910
const shuffledArray = JSON.parse(JSON.stringify(array));
@@ -73,14 +74,16 @@ function Curators(): JSX.Element {
7374
<div className="flex justify-evenly items-center flex-col flex-wrap">
7475
<div className="grid px-0 lg:px-12">
7576
{curators.map((curator, index) => {
77+
const curatorUrlWithTrackingParams = getUrlWithUtmTrackingParams({ url: curator.links.website });
78+
7679
return (
7780
<div className="flex sm:flex-col md:flex-row flex-wrap pb-8" key={index}>
7881
<div
7982
className={`shrink mr-4 mb-4 p-1 bg-gradient-to-br from-${theme}-300 to-${theme}-700 rounded transition duration-300`}
8083
>
8184
<div className={`bg-${theme}-100 p-1 rounded`}>
8285
<a
83-
href={curator.links.website}
86+
href={curatorUrlWithTrackingParams}
8487
target="_blank"
8588
rel="noreferrer"
8689
className="flex transition duration-700 hover:scale-105"
@@ -102,7 +105,7 @@ function Curators(): JSX.Element {
102105
</div>
103106
<div className="flex flex-col">
104107
<a
105-
href={curator.links.website}
108+
href={curatorUrlWithTrackingParams}
106109
target="_blank"
107110
rel="noreferrer"
108111
className={`text-${theme}-800 hover:underline`}

components/GIFItem.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Image from 'next/image';
22
import Gif from '../interfaces/gif';
33
import { useThemeState } from '../theme/ThemeContext';
4-
import { getMediaFormat } from '../utils';
4+
import { getMediaFormat, getUrlWithUtmTrackingParams } from '../utils';
55
import Text from './common/Text';
66
import Markdown from './Markdown';
77

@@ -22,6 +22,8 @@ const GIFItem = ({ gif }: { gif: Gif }): JSX.Element => {
2222
const renderMedia = ({ format, source, caption }: RenderMediaProps): JSX.Element => {
2323
switch (format) {
2424
case 'mp4':
25+
const sourceWithTrackingParams = getUrlWithUtmTrackingParams({ url: source });
26+
2527
return (
2628
<video
2729
autoPlay
@@ -34,9 +36,10 @@ const GIFItem = ({ gif }: { gif: Gif }): JSX.Element => {
3436
title={caption}
3537
preload="metadata"
3638
>
37-
<source src={source} type="video/mp4" />
39+
<source src={sourceWithTrackingParams} type="video/mp4" />
3840
<p>
39-
Your browser doesn&apos;t support HTML5 video. Here is a <a href={source}>link to the video</a> instead.
41+
Your browser doesn&apos;t support HTML5 video. Here is a{' '}
42+
<a href={sourceWithTrackingParams}>link to the video</a> instead.
4043
</p>
4144
</video>
4245
);

components/ToolItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import Image from 'next/image';
22
import ArticleItem from './ArticleItem';
33
import Tool from '../interfaces/tool';
4+
import { getUrlWithUtmTrackingParams } from '../utils';
45

56
const ToolItem = ({ tool: { title, url, logo, authors, desc, tags } }: { tool: Tool }): JSX.Element => {
67
const article = { title, url, desc, authors, tags };
78

9+
const toolUrlWithTrackingParams = getUrlWithUtmTrackingParams({ url });
10+
811
return (
912
<div className="flex flex-wrap sm:flex-nowrap flex-row items-center">
1013
<a
11-
href={url}
14+
href={toolUrlWithTrackingParams}
1215
className="w-full mr-8 max-w-[fit-content] my-4 p-1 transition duration-700 hover:scale-105 img-shadow-sm hover:img-shadow-none"
1316
target="_blank"
1417
rel="noreferrer"

components/common/SocialShare.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
22

33
import { CopyIcon, CheckSqaureIcon, TwitterIcon, ShareIcon, WhatsAppIcon, FacebookIcon } from '../icons/icons';
44
import { useThemeState } from '../../theme/ThemeContext';
5+
import { getUrlWithUtmTrackingParams } from '../../utils';
56

67
interface SocialShareProps {
78
url?: string;
@@ -23,10 +24,12 @@ interface ShareLinkProps {
2324
}
2425

2526
export const ShareLink = ({ text, label, url, icon, showText }: ShareLinkProps): JSX.Element => {
27+
const shareUrlWithTrackingParams = getUrlWithUtmTrackingParams({ url });
28+
2629
return (
2730
<a
2831
aria-label={label}
29-
href={url}
32+
href={shareUrlWithTrackingParams}
3033
className="transition duration-500 ease-in-out hover:scale-125"
3134
target="_blank"
3235
rel="noreferrer"

scripts/issueEmailGenerator.js

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -99,87 +99,112 @@ if ('issueNumber' in options && typeof options.issueNumber === 'number') {
9999
}
100100
}
101101

102+
const getUrlWithUtmTrackingParams = ({ url, medium = 'newsletter' }) => {
103+
const utmParams = {
104+
utm_source: 'scriptified.dev',
105+
utm_medium: medium,
106+
};
107+
108+
try {
109+
const urlWithParams = new URL(url);
110+
111+
Object.keys(utmParams).forEach(key => {
112+
urlWithParams.searchParams.set(key, utmParams[key]);
113+
});
114+
115+
return urlWithParams.toString();
116+
} catch (err) {
117+
console.error(err);
118+
// return url as is if URL is invalid
119+
return url;
120+
}
121+
};
122+
102123
const ogImgURL = getOGImage(currentIssue.title, currentIssue.id, currentIssue.date);
103124

125+
const issueWebUrl = getUrlWithUtmTrackingParams({ url: `https://scriptified.dev/issues/${currentIssue.id}` });
126+
104127
const emailTemplate = `
105128
![Headshot](${ogImgURL})
106129
107-
<center>[Read issue on web](https://scriptified.dev/issues/${currentIssue.id})</center>
130+
<center>[Read issue on web](${issueWebUrl})</center>
108131
109132
${currentIssue.description}
110133
111134
# Tip of the day
112135
113136
${currentIssue.tip_of_the_week.description}
114137
115-
${currentIssue.tip_of_the_week.codeSnippet &&
116-
`
138+
${
139+
currentIssue.tip_of_the_week.codeSnippet &&
140+
`
117141
\`\`\`${currentIssue.tip_of_the_week.codeSnippet.language}
118142
${currentIssue.tip_of_the_week.codeSnippet.code.code}
119143
\`\`\`
120144
`
121-
}
145+
}
122146
123147
${getAuthors(currentIssue.tip_of_the_week?.authors)}
124148
___
125149
126150
# Articles
127151
128152
${currentIssue.articles
129-
.map(
130-
article =>
131-
`[**${article.title}**](${article.url})
153+
.map(
154+
article =>
155+
`[**${article.title}**](${getUrlWithUtmTrackingParams({ url: article.url })})
132156
133157
${article.description}
134158
135159
${getAuthors(article?.authors)}
136160
`
137-
)
138-
.join('\n')}
161+
)
162+
.join('\n')}
139163
140164
___
141165
142-
${currentIssue.devOfTheWeek
143-
? `# Dev of the Week
166+
${
167+
currentIssue.devOfTheWeek
168+
? `# Dev of the Week
144169
145170
<img alt="${currentIssue.devOfTheWeek.name}" src="${getAssetURL(
146-
currentIssue.id,
147-
currentIssue.devOfTheWeek.profileImg
148-
)}" style="width:200px;"/>
171+
currentIssue.id,
172+
currentIssue.devOfTheWeek.profileImg
173+
)}" style="width:200px;"/>
149174
150175
## ${currentIssue.devOfTheWeek.name}
151176
${currentIssue.devOfTheWeek.bio}
152177
153178
${Object.keys(currentIssue.devOfTheWeek)
154-
.filter(key => PROFILE_KEYS.includes(key) && currentIssue.devOfTheWeek[key] !== null)
155-
.map(profile => `[${PROFILE_TYPES[profile]}](${currentIssue.devOfTheWeek[profile]})`)
156-
.join(' | ')}
179+
.filter(key => PROFILE_KEYS.includes(key) && currentIssue.devOfTheWeek[key] !== null)
180+
.map(profile => `[${PROFILE_TYPES[profile]}](${currentIssue.devOfTheWeek[profile]})`)
181+
.join(' | ')}
157182
158183
___`
159-
: ''
160-
}
184+
: ''
185+
}
161186
162187
# Tools
163188
164189
${currentIssue.tools
165-
.map(
166-
tool =>
167-
`[**${tool.name}**](${tool.url})
190+
.map(
191+
tool =>
192+
`[**${tool.name}**](${getUrlWithUtmTrackingParams({ url: tool.url })})
168193
169194
${tool.description}
170195
171196
${getAuthors(tool?.authors)}
172197
`
173-
)
174-
.join('\n')}
198+
)
199+
.join('\n')}
175200
176201
___
177202
178203
# Tech Talks
179204
180-
[**${currentIssue.talks[0].title}**](${currentIssue.talks[0].url})
205+
[**${currentIssue.talks[0].title}**](${getUrlWithUtmTrackingParams({ url: currentIssue.talks[0].url })})
181206
182-
${currentIssue.talks[0].url}
207+
${getUrlWithUtmTrackingParams({ url: currentIssue.talks[0].url })})
183208
184209
${currentIssue.talks[0].description}
185210
@@ -195,9 +220,16 @@ ___
195220
${currentIssue.quiz.codeSnippet.code.code}
196221
\`\`\`
197222
198-
${currentIssue.quiz.options.map(
199-
option => `[<div style="margin: 12px 0px; border: 1px solid gray; padding: 16px; background: #F2F3F5;">${option.text.split('\n').join('<br>')}</div>](https://scriptified.dev/issues/${currentIssue.id}?section=quiz&option=${option.option_id})`
200-
).join('\n')}
223+
${currentIssue.quiz.options
224+
.map(
225+
option =>
226+
`[<div style="margin: 12px 0px; border: 1px solid gray; padding: 16px; background: #F2F3F5;">${option.text
227+
.split('\n')
228+
.join('<br>')}</div>](https://scriptified.dev/issues/${currentIssue.id}?section=quiz&option=${
229+
option.option_id
230+
})`
231+
)
232+
.join('\n')}
201233
202234
203235
___
@@ -213,8 +245,8 @@ ___
213245
Liked this issue? [Share on Twitter](https://twitter.com/intent/tweet?text=${encodeURIComponent(`Have a look at issue #${currentIssue.id} of Scriptified.
214246
215247
Subscribe to @scriptified_dev for more.`)}&url=${encodeURIComponent(
216-
`https://scriptified.dev/issues/${currentIssue.id}`
217-
)}) or [read previous issues](https://scriptified.dev/issues).
248+
`${issueWebUrl}`
249+
)}) or [read previous issues](https://scriptified.dev/issues).
218250
`;
219251
const archiveDirectory = './archives';
220252
const issueFile = `${archiveDirectory}/issue${currentIssue.id}.md`;

utils/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,44 @@ export const getMediaFormat = (mediaUrl: string): string => {
5454
return mediaFormat;
5555
}
5656
};
57+
58+
/* =================================================================== */
59+
60+
type UtmMedium = 'newsletter' | 'website';
61+
62+
interface UtmParams {
63+
utm_source: string;
64+
utm_medium: UtmMedium;
65+
}
66+
67+
/**
68+
* Get URL with UTM tracking params
69+
* @param url URL to be appended with UTM tracking params
70+
* @returns {string} URL with UTM tracking params
71+
*/
72+
export const getUrlWithUtmTrackingParams = ({
73+
url,
74+
medium = 'website',
75+
}: {
76+
url: string;
77+
medium?: UtmMedium;
78+
}): string => {
79+
const utmParams: UtmParams = {
80+
utm_source: 'scriptified.dev',
81+
utm_medium: medium,
82+
};
83+
84+
try {
85+
const urlWithParams = new URL(url);
86+
87+
Object.keys(utmParams).forEach(key => {
88+
urlWithParams.searchParams.set(key, utmParams[key]);
89+
});
90+
91+
return urlWithParams.toString();
92+
} catch (err) {
93+
console.error(err);
94+
// return url as is if URL is invalid
95+
return url;
96+
}
97+
};

0 commit comments

Comments
 (0)