Skip to content

Commit

Permalink
Merge pull request #26969 from guardian/ei/affiliate-links-allowlist
Browse files Browse the repository at this point in the history
Enable affiliate links for articles on an allowlist
  • Loading branch information
emma-imber authored Mar 15, 2024
2 parents cf99c27 + ba84daf commit 274f504
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 93 deletions.
18 changes: 17 additions & 1 deletion applications/app/views/fragments/galleryBody.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
@import views.support.`package`.Seq2zipWithRowInfo
@import views.support.{RenderClasses, RowInfo}
@import views.GalleryCaptionCleaners
@import views.support.AffiliateLinksCleaner
@import conf.switches.Switches
@import conf.Configuration

@import model.DotcomContentType
@(page: GalleryPage)(implicit request: RequestHeader, context: model.ApplicationContext)
Expand All @@ -19,7 +22,20 @@
)"
itemscope itemtype="@page.item.metadata.schemaType" role="main">

@fragments.galleryHeader(page)
@fragments.galleryHeader(
page,
page.item.lightbox.containsAffiliateableLinks && AffiliateLinksCleaner.shouldAddAffiliateLinks(
switchedOn = Switches.AffiliateLinks.isSwitchedOn,
section = page.gallery.content.metadata.sectionId,
showAffiliateLinks = page.gallery.content.fields.showAffiliateLinks,
supportedSections = Configuration.affiliateLinks.affiliateLinkSections,
defaultOffTags = Configuration.affiliateLinks.defaultOffTags,
alwaysOffTags = Configuration.affiliateLinks.alwaysOffTags,
tagPaths = page.gallery.content.tags.tags.map(_.id),
firstPublishedDate = page.gallery.content.fields.firstPublicationDate,
pageUrl = request.uri,
)
)

<div class="@RenderClasses(
"l-side-margins", "l-side-margins--media", "l-side-margins--gallery"
Expand Down
7 changes: 6 additions & 1 deletion applications/app/views/fragments/galleryHeader.scala.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@(gallery: model.GalleryPage)(implicit request: RequestHeader)
@(gallery: model.GalleryPage,
shouldAddDisclaimer: Boolean)(implicit request: RequestHeader)

@import views.support.TrailCssClasses.toneClass

Expand Down Expand Up @@ -44,6 +45,10 @@ <h1 class="is-hidden" itemprop="headline">@Html(gallery.item.trail.headline)</h1
@if(!gallery.item.trail.shouldHidePublicationDate) {
@fragments.meta.dateline(gallery.item.trail.webPublicationDate, gallery.item.fields.lastModified, gallery.item.content.hasBeenModified, gallery.item.fields.firstPublicationDate, gallery.item.tags.isLiveBlog, gallery.item.fields.isLive)
}

@if(shouldAddDisclaimer) {
@fragments.affiliateLinksDisclaimer()
}
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion applications/app/views/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ object GalleryCaptionCleaners {
request.uri,
page.gallery.content.metadata.sectionId,
page.gallery.content.fields.showAffiliateLinks,
"gallery",
appendDisclaimer = Some(isFirstRow && page.item.lightbox.containsAffiliateableLinks),
tags = page.gallery.content.tags.tags.map(_.id),
page.gallery.content.fields.firstPublicationDate,
Expand Down
1 change: 0 additions & 1 deletion article/app/views/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ object BodyProcessor {
pageUrl = request.uri,
sectionId = article.content.metadata.sectionId,
showAffiliateLinks = article.content.fields.showAffiliateLinks,
contentType = "article",
tags = article.content.tags.tags.map(_.id),
publishedDate = article.content.fields.firstPublicationDate,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import navigation._
import play.api.libs.json._
import play.api.mvc.RequestHeader
import services.NewsletterData
import views.html.fragments.affiliateLinksDisclaimer
import views.support.{CamelCase, ContentLayout, JavaScriptPage}
// -----------------------------------------------------------------
// DCR DataModel
Expand Down Expand Up @@ -582,16 +581,16 @@ object DotcomRenderingDataModel {

val selectedTopics = topicResult.map(topic => Seq(Topic(topic.`type`, topic.name)))

def getAffiliateLinksDisclaimer(shouldAddAffiliateLinks: Boolean, shouldAddDisclaimer: Boolean) = {
def addAffiliateLinksDisclaimerDCR(shouldAddAffiliateLinks: Boolean, shouldAddDisclaimer: Boolean) = {
if (shouldAddAffiliateLinks && shouldAddDisclaimer) {
Some(affiliateLinksDisclaimer("article").body)
Some("true")
} else {
None
}
}

DotcomRenderingDataModel(
affiliateLinksDisclaimer = getAffiliateLinksDisclaimer(shouldAddAffiliateLinks, shouldAddDisclaimer),
affiliateLinksDisclaimer = addAffiliateLinksDisclaimerDCR(shouldAddAffiliateLinks, shouldAddDisclaimer),
author = author,
badge = Badges.badgeFor(content).map(badge => DCRBadge(badge.seriesTag, badge.imageUrl)),
beaconURL = Configuration.debug.beaconUrl,
Expand Down
31 changes: 3 additions & 28 deletions common/app/model/dotcomrendering/DotcomRenderingUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import common.Edition
import conf.switches.Switches
import conf.{Configuration, Static}
import model.content.Atom
import model.dotcomrendering.pageElements.{DisclaimerBlockElement, PageElement, TextCleaner}
import model.dotcomrendering.pageElements.{PageElement, TextCleaner}
import model.pressed.{PressedContent, SpecialReport}
import model.{
ArticleDateTimes,
Expand All @@ -24,7 +24,6 @@ import model.{
import org.joda.time.format.DateTimeFormat
import play.api.libs.json._
import play.api.mvc.RequestHeader
import views.html.fragments.affiliateLinksDisclaimer
import views.support.AffiliateLinksCleaner

import java.net.URLEncoder
Expand Down Expand Up @@ -175,30 +174,6 @@ object DotcomRenderingUtils {
val ids = liveblog.currentPage.currentPage.blocks.map(_.id).toSet
relevantBlocks.filter(block => ids(block.id))
}.toSeq

private def addDisclaimer(
elems: List[PageElement],
capiElems: Seq[ClientBlockElement],
affiliateLinks: Boolean,
): List[PageElement] = {
if (affiliateLinks) {
val hasLinks = capiElems.exists(elem =>
elem.`type` match {
case Text =>
val textString = elem.textTypeData.toList.mkString("\n") // just concat all the elems here for this test
stringContainsAffiliateableLinks(textString)
case _ => false
},
)

if (hasLinks) {
elems :+ DisclaimerBlockElement(affiliateLinksDisclaimer("article").body)
} else {
elems
}
} else elems
}

def stringContainsAffiliateableLinks(textString: String): Boolean = {
AffiliateLinksCleaner.stringContainsAffiliateableLinks(textString)
}
Expand Down Expand Up @@ -238,8 +213,7 @@ object DotcomRenderingUtils {
val withTagLinks =
if (article.content.isPaidContent) elems
else TextCleaner.tagLinks(elems, article.content.tags, article.content.showInRelated, edition)

addDisclaimer(withTagLinks, capiElems, affiliateLinks)
elems
}

def isSpecialReport(page: ContentPage): Boolean =
Expand Down Expand Up @@ -276,6 +250,7 @@ object DotcomRenderingUtils {
alwaysOffTags = Configuration.affiliateLinks.alwaysOffTags,
tagPaths = content.content.tags.tags.map(_.id),
firstPublishedDate = content.content.fields.firstPublicationDate,
pageUrl = content.metadata.id,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,6 @@ object ContentAtomBlockElement {
implicit val ContentAtomBlockElementWrites: Writes[ContentAtomBlockElement] = Json.writes[ContentAtomBlockElement]
}

case class DisclaimerBlockElement(html: String) extends PageElement
object DisclaimerBlockElement {
implicit val DisclaimerBlockElementWrites: Writes[DisclaimerBlockElement] = Json.writes[DisclaimerBlockElement]
}

case class DocumentBlockElement(
embedUrl: Option[String],
height: Option[Int],
Expand Down Expand Up @@ -821,7 +816,6 @@ object PageElement {
case _: CodeBlockElement => true
case _: CommentBlockElement => true
case _: ContentAtomBlockElement => true
case _: DisclaimerBlockElement => true
case _: DocumentBlockElement => true
case _: EmbedBlockElement => true
case _: ExplainerAtomBlockElement => true
Expand Down
44 changes: 10 additions & 34 deletions common/app/views/fragments/affiliateLinksDisclaimer.scala.html
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
@(contentType: String)


@articleDisclaimer() = {
<p><sup>
The Guardian’s product and service reviews are independent and are
in no way influenced by any advertiser or commercial initiative. We
will earn a commission from the retailer if you buy something
through an affiliate link.
<a
href="https://www.theguardian.com/info/2017/nov/01/reader-information-on-affiliate-links"
data-link-name="in body link"
class="u-underline"
>Learn more</a
>.
</sup></p>
}

@galleryDisclaimer() = {
<br>
The Guardian’s product and service reviews are independent and are in no
way influenced by any advertiser or commercial initiative. We will earn a
commission from the retailer if you buy something through an affiliate link.
<a
href="https://www.theguardian.com/info/2017/nov/01/reader-information-on-affiliate-links"
>Learn more</a
>.
}

@{contentType match {
case "gallery" => galleryDisclaimer()
case "article" => articleDisclaimer()
case _ => Html("")
}}
<br>
<p class="gallery__disclaimer">
The Guardian’s product and service reviews are independent and are in no
way influenced by any advertiser or commercial initiative. We will earn a
commission from the retailer if you buy something through an affiliate link.
<a
href="https://www.theguardian.com/info/2017/nov/01/reader-information-on-affiliate-links"
>Learn more</a
>.
</p>
52 changes: 35 additions & 17 deletions common/app/views/support/HtmlCleaner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,6 @@ case class AffiliateLinksCleaner(
pageUrl: String,
sectionId: String,
showAffiliateLinks: Option[Boolean],
contentType: String,
appendDisclaimer: Option[Boolean] = None,
tags: List[String],
publishedDate: Option[DateTime],
Expand All @@ -897,9 +896,10 @@ case class AffiliateLinksCleaner(
alwaysOffTags,
tags,
publishedDate,
pageUrl,
)
) {
AffiliateLinksCleaner.replaceLinksInHtml(document, pageUrl, appendDisclaimer, contentType, skimlinksId)
AffiliateLinksCleaner.replaceLinksInHtml(document, pageUrl, skimlinksId)
} else document
}
}
Expand All @@ -912,19 +912,14 @@ object AffiliateLinksCleaner {
def replaceLinksInHtml(
html: Document,
pageUrl: String,
appendDisclaimer: Option[Boolean],
contentType: String,
skimlinksId: String,
): Document = {
val linksToReplace: mutable.Seq[Element] = getAffiliateableLinks(html)
linksToReplace.foreach { el => el.attr("href", linkToSkimLink(el.attr("href"), pageUrl, skimlinksId)) }
// respect appendDisclaimer (for Galleries), or if it's not set then always add the disclaimer if affilate links have been added
val shouldAppendDisclaimer = appendDisclaimer.getOrElse(linksToReplace.nonEmpty)
if (shouldAppendDisclaimer) insertAffiliateDisclaimer(html, contentType)
else html
html
}

def replaceLinksInElement(html: String, pageUrl: String, contentType: String): TextBlockElement = {
def replaceLinksInElement(html: String, pageUrl: String): TextBlockElement = {
val doc = Jsoup.parseBodyFragment(html)
val linksToReplace: mutable.Seq[Element] = getAffiliateableLinks(doc)
linksToReplace.foreach { el => el.attr("href", linkToSkimLink(el.attr("href"), pageUrl, skimlinksId)) }
Expand All @@ -939,11 +934,6 @@ object AffiliateLinksCleaner {
def isAffiliatable(element: Element): Boolean =
element.tagName == "a" && SkimLinksCache.isSkimLink(element.attr("href"))

def insertAffiliateDisclaimer(document: Document, contentType: String): Document = {
document.body().append(affiliateLinksDisclaimer(contentType).toString())
document
}

def linkToSkimLink(link: String, pageUrl: String, skimlinksId: String): String = {
val urlEncodedLink = URLEncode(link)
s"https://go.skimresources.com/?id=$skimlinksId&url=$urlEncodedLink&sref=$host$pageUrl"
Expand All @@ -962,13 +952,41 @@ object AffiliateLinksCleaner {
alwaysOffTags: Set[String],
tagPaths: List[String],
firstPublishedDate: Option[DateTime],
pageUrl: String,
): Boolean = {
val publishedCutOffDate = new DateTime(2020, 8, 14, 0, 0)

// Never include affiliate links if it is tagged with an always off tag, or if it was published before our cut off
// date. The cut off date is temporary while we are working on improving the compliance of affiliate links
val cleanedPageUrl = if (pageUrl.charAt(0) == '/') {
pageUrl.substring(1);
} else pageUrl

val affiliateLinksAllowList = List(
"lifeandstyle/2024/jan/03/six-winter-warmers-tried-and-tested-the-heated-poncho-has-changed-me-i-will-never-have-sex-again",
"lifeandstyle/2024/mar/11/im-south-asian-and-have-dark-eye-circles-what-can-i-do",
"fashion/2024/mar/08/the-four-makeup-staples-i-cant-live-without",
"travel/2023/mar/03/readers-favourite-budget-beach-campsites-hotels-in-europe",
"travel/2024/feb/25/10-of-the-best-places-in-the-uk-to-see-them-bloom",
"lifeandstyle/2023/dec/10/with-christmas-around-the-corner-what-to-give-the-gardener-in-your-life-",
"fashion/2024/mar/01/spring-is-around-the-corner-time-to-soothe-and-restore-your-cracked-heels",
"fashion/2024/mar/10/compact-and-bijou-why-women-need-a-pocket-mirror",
"fashion/2024/mar/03/how-to-reset-your-wardrobe-for-spring",
"lifeandstyle/2024/mar/03/beauty-spot-eyebrow-essentials-10-of-the-best",
"fashion/gallery/2024/mar/09/spring-in-your-step-10-menswear-trends-to-update-your-wardrobe-in-pictures",
"fashion/gallery/2024/mar/08/street-smart-what-to-wear-to-run-errands",
"fashion/gallery/2024/mar/09/the-edit-mens-sweatshirts-in-pictures",
"lifeandstyle/gallery/2024/jan/22/colourful-glass-furniture-from-vases-to-lampshades-in-pictures",
"lifeandstyle/gallery/2023/nov/27/cosy-bedding-in-pictures",
)

val urlIsInAllowList = affiliateLinksAllowList.contains(cleanedPageUrl)

// Never include affiliate links if it is tagged with an always off tag, or if it was published before our cut off date.
// The cut off date is temporary while we are working on improving the compliance of affiliate links.
// The cut off date does not apply to any URL on the allow list
if (
!contentHasAlwaysOffTag(tagPaths, alwaysOffTags) && firstPublishedDate.exists(_.isBefore(publishedCutOffDate))
!contentHasAlwaysOffTag(tagPaths, alwaysOffTags) && (firstPublishedDate.exists(
_.isBefore(publishedCutOffDate),
) || urlIsInAllowList)
) {
if (showAffiliateLinks.isDefined) {
showAffiliateLinks.contains(true)
Expand Down
Loading

0 comments on commit 274f504

Please sign in to comment.