Add UTM parameter support for shortened links#997
Add UTM parameter support for shortened links#997seler wants to merge 2 commits intothedevs-network:mainfrom
Conversation
Allow users to set UTM parameters (source, medium, campaign, term, content) when creating or editing links. Parameters are appended to the target URL on redirect.
There was a problem hiding this comment.
Pull request overview
Adds first-class UTM parameter support to the link shortener so creators can store campaign metadata on a link and have it automatically appended to the target URL during redirects.
Changes:
- Adds
utm_source,utm_medium,utm_campaign,utm_term,utm_contentfields to create/edit UIs and request validation. - Persists UTM fields on link creation/edit and appends them to the redirect destination via
buildTargetURL(). - Introduces a DB migration adding 5 nullable UTM columns to the
linkstable and updates link selection/creation queries to include them.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/views/partials/shortener.hbs | Adds UTM inputs to the “advanced options” section of the shortener form. |
| server/views/partials/links/edit.hbs | Adds UTM inputs to the user link edit form. |
| server/views/partials/admin/links/edit.hbs | Adds UTM inputs to the admin link edit form. |
| server/queries/link.queries.js | Includes UTM columns in selectable fields and persists them on link creation. |
| server/migrations/20260325180240_utm_params.js | Adds UTM columns to the links table (and defines a rollback). |
| server/handlers/validators.handler.js | Adds validation/sanitization for all UTM fields on create/edit. |
| server/handlers/links.handler.js | Builds redirect target URL with stored UTMs; wires UTM fields into create/edit + redirect flows. |
Comments suppressed due to low confidence (2)
server/handlers/links.handler.js:220
- In the edit flow,
if (!value) { delete req.body[name]; }treats empty strings as “not provided”. That makes it impossible to clear an existing UTM field from a link (submitting an empty input deletes the key, so the subsequentutm_* !== undefinedupdate never runs). Consider handlingutm_*(and any other clearable optional fields) explicitly: interpret""asnulland keep the key so it can be persisted, and ensureisChangedis set when the stored value is being cleared.
].forEach(([value, name]) => {
if (!value) {
if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
return;
server/handlers/links.handler.js:325
- Same issue as the user edit handler: the
if (!value) { delete req.body[name]; }logic prevents clearing existingutm_*values (empty input results in the field being removed fromreq.body, so it won’t be updated tonull). Handle empty-string submissions for UTM fields explicitly (e.g., normalize""tonulland keep the key) so admins can remove previously set UTM params.
].forEach(([value, name]) => {
if (!value) {
if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
return;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
server/handlers/links.handler.js:324
- Same issue as the non-admin edit path: the
if (!value) { delete req.body[name]; }block will drop falsyutm_*values, so admins can’t clear existing UTM parameters (andnullwon’t persist either). Consider normalizing""tonulland allowing explicit nulling for the UTM fields rather than deleting them.
if (!value) {
if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
return;
server/handlers/links.handler.js:220
- The edit change-detection logic deletes any falsy field (
if (!value) { delete req.body[name]; }). For the newutm_*fields this prevents clearing a previously-set value (empty string / null gets deleted, so the update never writesNULL). If clearing should be supported, handle UTM fields (and any other nullable fields) explicitly by converting""tonulland treating that as a change instead of deleting the key.
if (!value) {
if (name === "password" && link.password)
req.body.password = null;
else {
delete req.body[name];
return;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| table.string("utm_source"); | ||
| table.string("utm_medium"); | ||
| table.string("utm_campaign"); | ||
| table.string("utm_term"); | ||
| table.string("utm_content"); |
There was a problem hiding this comment.
The validators enforce a max length of 255 for UTM fields, but the migration defines them as table.string("utm_*" ) without an explicit length. To keep DB constraints aligned with validation across different DB backends, consider specifying the length explicitly (e.g., table.string("utm_source", 255) etc.).
| table.string("utm_source"); | |
| table.string("utm_medium"); | |
| table.string("utm_campaign"); | |
| table.string("utm_term"); | |
| table.string("utm_content"); | |
| table.string("utm_source", 255); | |
| table.string("utm_medium", 255); | |
| table.string("utm_campaign", 255); | |
| table.string("utm_term", 255); | |
| table.string("utm_content", 255); |
| module.exports = { | ||
| up, | ||
| down | ||
| } |
There was a problem hiding this comment.
Migration file formatting is inconsistent with other migrations: module.exports typically ends with }; (see e.g. server/migrations/20250106070444_remove_cooldown.js). Consider adding the trailing semicolon here for consistency.
| } | |
| }; |
| const { reuse, password, customurl, description, target, fetched_domain, expire_in, | ||
| utm_source, utm_medium, utm_campaign, utm_term, utm_content } = req.body; | ||
| const domain_id = fetched_domain ? fetched_domain.id : null; | ||
|
|
||
| const targetDomain = utils.removeWww(URL.parse(target).hostname); |
There was a problem hiding this comment.
reuse mode currently looks up an existing link by { target, user_id, domain_id } only. With UTM fields now affecting the final redirect URL, this can return an existing link whose stored UTM parameters differ from the ones submitted, so the response/redirect won’t reflect the requested UTM values. Consider including the UTM fields in the reuse lookup (or disabling reuse when any UTM field is present) so “reuse” only triggers when the redirect behavior would be identical.
|
Any chance of review from human? :D |
buildTargetURL()appends them to the target URL.links.