Altering href anchor links targets

Working through an issue where the data contents of the HTML has href anchor links that are not consistently target to open in a blank window. The reason to do this is that the original HTML content is web view of an email, therefore having it follow links instead of opening in new browser windows is confusing for back and forth navigation. Better to force all links to open in a new window with the target="_blank"

Approach 1: Adjust HTML prior to sending to browser for rendering

Use HtmlAgilityPack to extract out the external links and then set their targets. This is an extension method so it can added in a fluent manner.

public static string SetExternalLinkTargets(this string inputHtml, string target = "_blank")
{
	if (inputHtml == null)
	{
		return null;
	}
	var htmlContent = new HtmlDocument();
	htmlContent.LoadHtml(inputHtml);
	if (string.IsNullOrEmpty(htmlContent.Text))
	{
		return inputHtml;
	}
	var links = htmlContent.DocumentNode.SelectNodes("//a");
	foreach (var link in links)
	{
		var linkUrl = link.GetAttributeValue("href", "");
		if (linkUrl.StartsWith("~/"))
		{
			continue;
		}
		link.SetAttributeValue("target", target);
	}
	return htmlContent.DocumentNode.OuterHtml;
}

This means that you can call the sample html as a string.

var testHtml = @"<html><body style=""font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; ""><p>Hi Brendon,</p><p>Something I wanted to mention about your booking at ThePlace...</p><a href=""https://thisisalink.invalid/superlungs"">'Superlungs'</a></body></html>";

and to transform the links within the document, do this

var result = testHtml.SetExternalLinkTargets();

Unit Tests for this approach

[Fact]
public void SetExternalLinks()
{
	// empty string
	string nullInput = null;
	var nullInputSetLink = nullInput.SetExternalLinkTargets();
	Assert.Equal(nullInput, nullInputSetLink);            
	
	// empty string
	var emptyString = $"";
	Assert.DoesNotContain("target=\"_blank\"", emptyString);
	var emptyStringSetLink = emptyString.SetExternalLinkTargets();
	Assert.Equal(emptyString, emptyStringSetLink);
	
	// href only
	var hrefAbsoluteUrlOnly = $"<a href=\"{Faker.Internet.Url()}\">${Faker.Lorem.GetFirstWord()}</a>";
	Assert.DoesNotContain("target=\"_blank\"", hrefAbsoluteUrlOnly);
	var hrefAbsoluteUrlExternal = hrefAbsoluteUrlOnly.SetExternalLinkTargets();
	Assert.NotEqual(hrefAbsoluteUrlOnly, hrefAbsoluteUrlExternal);
	Assert.Contains("target=\"_blank\"", hrefAbsoluteUrlExternal);
	
	// href set to relative
	var hrefRelativeUrlOnly = $"<a href=\"~/localpath/{Faker.Lorem.GetFirstWord()}\">${Faker.Lorem.GetFirstWord()}</a>";
	Assert.DoesNotContain("target=\"_blank\"", hrefRelativeUrlOnly);
	var hrefRelativeUrlExternal = hrefRelativeUrlOnly.SetExternalLinkTargets();
	Assert.Equal(hrefRelativeUrlOnly, hrefRelativeUrlExternal);
	
	// Full html document
	const string htmlInput =
		@"<html><body style=""font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; ""><p>Hi Brendon,</p><p>Something I wanted to mention about your booking at ThePlace...</p><a href=""https://thisisalink.invalid/superlungs"">'Superlungs'</a><p>-Test</p><img src=""https://thisisalink.invalid/gorgosaurus-and-citipes.jpg""><div style=""color: #999; font-family: Helvetica Neue,Helvetica,Arial,sans-serif;""><div style=""padding-bottom: 12px;""></div><div style=""float: left; border-right: solid 1px #b1b1b1; padding-right: 20px; margin-right: 20px; padding: 5px 30px 5px 5px;"">Email or call:<br><a href=""mailto:testuser@test.com""><img style=""vertical-align: middle; width: 17px; height: 17px; border: none; margin-right: 5px;"" src=""https://testsite1.com/content-nonversioned/themes/envelope-solid.png"" alt=""Testing & Business email icon""/>testuser@test.com</a></div><div style=""width: 325px; float: left; padding: 5px;"">Visit us online:<br><a href=""https://powpow.com""><img style=""vertical-align: middle; width: 17px; height: 17px; border: none; margin-right: 5px;"" src=""https://testsite1.com/content-nonversioned/themes/desktop-solid.png"" alt=""Testing & Business website icon""/>powpow.com</a></div></div></body></html>";
	var withTargetBlanks = htmlInput.SetExternalLinkTargets();
	
	const string htmlExpectedOutput =
		@"<html><body style=""font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; ""><p>Hi Brendon,</p><p>Something I wanted to mention about your booking at ThePlace...</p><a href=""https://thisisalink.invalid/superlungs"" target=""_blank"">'Superlungs'</a><p>-Test</p><img src=""https://thisisalink.invalid/gorgosaurus-and-citipes.jpg""><div style=""color: #999; font-family: Helvetica Neue,Helvetica,Arial,sans-serif;""><div style=""padding-bottom: 12px;""></div><div style=""float: left; border-right: solid 1px #b1b1b1; padding-right: 20px; margin-right: 20px; padding: 5px 30px 5px 5px;"">Email or call:<br><a href=""mailto:testuser@test.com"" target=""_blank""><img style=""vertical-align: middle; width: 17px; height: 17px; border: none; margin-right: 5px;"" src=""https://testsite1.com/content-nonversioned/themes/envelope-solid.png"" alt=""Testing & Business email icon"">testuser@test.com</a></div><div style=""width: 325px; float: left; padding: 5px;"">Visit us online:<br><a href=""https://powpow.com"" target=""_blank""><img style=""vertical-align: middle; width: 17px; height: 17px; border: none; margin-right: 5px;"" src=""https://testsite1.com/content-nonversioned/themes/desktop-solid.png"" alt=""Testing & Business website icon"">powpow.com</a></div></div></body></html>";
	
	Assert.Equal(htmlExpectedOutput, withTargetBlanks);
}

Approach 2: Read the contents parse adjust the Document

Here the original content is loaded into an iFrame and a new attribute called data-url

  • Search for all the targetClass elements in the current HTML
  • Fetch the data from the new attribute data-url
  • When the content returns parse it as text and run through the DOMParser as text/html
  • Create a new document element for targets as default base HTML is like so
<head>
    <base target="_blank" />
</head>

// jquery missing reference so fallback to plain javascript
window.addEventListener('load', async function () {
  const emails = document.getElementsByClassName("targetClass");
  for (let i = 0; i < emails.length; i++) {
	  try {
		const currentEmail = emails[i];
		const dataUrl = currentEmail.getAttribute("data-url"); 
		const response = await fetch(dataUrl, { credentials: "include" });
		let pageHtml = await response.text();
		const parser = new DOMParser();
		const htmlDoc = parser.parseFromString(pageHtml, "text/html");
		let baseUrlTag = document.createElement("base");
		baseUrlTag.target = "_blank";
		htmlDoc.head.appendChild(baseUrlTag);
		currentEmail.srcdoc = htmlDoc.documentElement.innerHTML; 
	  } catch (exc) {
		  console.log(exc);
	  }
  }
});