❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️

Auto-append UTM parameters to Microsoft Learn links with an Astro Remark plugin

Auto-append UTM parameters to Microsoft Learn links with an Astro Remark plugin

Why UTM parameters matter

If you share links publicly, you usually want to know where traffic came from. UTM parameters are simple key/value query parameters you append to a URL, like ?utm_source=newsletter&utm_medium=email. Analytics systems (Google Analytics, Clarity, your own data warehouse) pick these up to attribute visits and conversions.

Companies use UTM parameters to:

  • Attribute traffic to campaigns and channels (email, social, events)
  • Compare which sources perform best (CTR, signups, purchases)
  • Prove impact for partners, ambassadors, and programs

You’ll also see similar tracking schemes in learning platforms, event sites, and shops. Microsoft Learn, for example, recognizes parameters like WT.mc_id and sharingId to attribute clicks to MVPs, speakers, or campaigns.

Manually typing UTM parameters into every MDX file is error-prone and tedious. You’ll forget, mistype, or change conventions later. Worse, you may end up with inconsistent tracking. The better approach is to add UTMs at build time with a small Markdown/MDX transformβ€”so authors can keep writing clean links.

The tiny plugin that does the job

Below is a minimal Remark plugin that automatically appends the expected parameters to any link pointing to learn.microsoft.com (or the legacy docs.microsoft.com). It leaves all other linksβ€”and any existing query params or hash fragmentsβ€”untouched.

// remark-mslearn-utm.mjs
// A tiny Remark plugin that appends UTM params to learn.microsoft.com / docs.microsoft.com links
// Parameters requested: WT.mc_id=email & sharingId=MVP_417906

/**
 * Append (or set) the required query params to a URL string.
 * Leaves other params and hashes intact. Returns the original string on parse errors.
 */
function appendUtm(urlStr) {
  try {
    const u = new URL(urlStr);
    // Only process Microsoft Learn domains
    const host = u.hostname.toLowerCase();
    if (!host.endsWith('learn.microsoft.com') && !host.endsWith('docs.microsoft.com')) {
      return urlStr;
    }
    u.searchParams.set('WT.mc_id', 'email');
    u.searchParams.set('sharingId', 'MVP_417906');
    return u.toString();
  } catch {
    return urlStr;
  }
}

/**
 * Minimal recursive walk without external deps to find Link nodes.
 */
function walk(node, visitor) {
  if (!node || typeof node !== 'object') return;
  visitor(node);
  const children = node.children;
  if (Array.isArray(children)) {
    for (const child of children) {
      walk(child, visitor);
    }
  }
}

export default function remarkMsLearnUtm() {
  return (tree) => {
    walk(tree, (node) => {
      // remark (mdast) link nodes have type 'link' and property 'url'
      if (node.type === 'link' && typeof node.url === 'string') {
        node.url = appendUtm(node.url);
      }
      // Also handle image references that might point to Learn (rare but safe)
      if (node.type === 'image' && typeof node.url === 'string') {
        node.url = appendUtm(node.url);
      }
    });
  };
}

Wire it into Astro once

Add the plugin to your Astro config so it runs for every MD/MDX file automatically. In this site, it’s already set up in astro.config.mjs:

import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import remarkMermaid from 'remark-mermaidjs';
import remarkMsLearnUtm from './scripts/remark-mslearn-utm.mjs';
// ...

export default defineConfig({
  // ...
  markdown: {
    remarkPlugins: [remarkMermaid, remarkMsLearnUtm],
    // ...
  },
});

That’s it. Authors can now write clean links:

[Adaptive Cards overview](https://learn.microsoft.com/microsoftteams/platform/task-modules-and-cards/cards)

And at build time, the output becomes (UTM added, any existing params preserved):

https://learn.microsoft.com/microsoftteams/platform/task-modules-and-cards/cards?WT.mc_id=email&sharingId=MVP_417906

Guardrails and edge cases

  • Only Microsoft Learn/Docs domains are touched; everything else is left as-is.
  • Existing query parameters and #hash fragments are preserved.
  • If a link is malformed, it’s ignored (the original URL stays unchanged).
  • Images are also checked, just in case a Learn URL appears there.

Why this approach scales

Centralizing tracking logic in a single build step means:

  • No more repetitive typing in MDX files
  • Consistent parameters across the whole site
  • Easy to change conventions (update one plugin)

If you need different UTMs per section or campaign, you can extend the plugin (e.g., vary by pathname, frontmatter, or environment variables).

TL;DR

Use a tiny Remark plugin to append UTM parameters automatically. You’ll get consistent analytics without cluttering your content.