diff --git a/applications/app/views/fragments/galleryBody.scala.html b/applications/app/views/fragments/galleryBody.scala.html index b0f69caf121..bd074fea597 100644 --- a/applications/app/views/fragments/galleryBody.scala.html +++ b/applications/app/views/fragments/galleryBody.scala.html @@ -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) @@ -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, + ) + )
- 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. - Learn more. -
-} - -@galleryDisclaimer() = { -+ 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. + Learn more. +
\ No newline at end of file diff --git a/common/app/views/support/HtmlCleaner.scala b/common/app/views/support/HtmlCleaner.scala index 3ce9a172ad2..a0d1fd0fe9c 100644 --- a/common/app/views/support/HtmlCleaner.scala +++ b/common/app/views/support/HtmlCleaner.scala @@ -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], @@ -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 } } @@ -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)) } @@ -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" @@ -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) diff --git a/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala b/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala index ceb5d04e95d..9205e868a94 100644 --- a/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala +++ b/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala @@ -19,6 +19,8 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { val supportedSections = Set("film", "books", "fashion") val oldPublishedDate = Some(new DateTime(2020, 8, 13, 0, 0)) val newPublishedDate = Some(new DateTime(2020, 8, 15, 0, 0)) + val allowedPageUrl = "/fashion/gallery/2024/mar/08/street-smart-what-to-wear-to-run-errands" + val deniedPageUrl = "/fashion/2024/feb/16/sunscreen-in-winter-yep-spf-moisturiser-is-essential-all-year-round" shouldAddAffiliateLinks( switchedOn = false, @@ -29,6 +31,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List.empty, oldPublishedDate, + deniedPageUrl, ) should be(false) shouldAddAffiliateLinks( switchedOn = true, @@ -39,6 +42,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List.empty, oldPublishedDate, + deniedPageUrl, ) should be(true) shouldAddAffiliateLinks( switchedOn = true, @@ -49,6 +53,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List.empty, oldPublishedDate, + deniedPageUrl, ) should be(false) shouldAddAffiliateLinks( switchedOn = true, @@ -59,6 +64,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List.empty, oldPublishedDate, + deniedPageUrl, ) should be(true) shouldAddAffiliateLinks( switchedOn = true, @@ -69,6 +75,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List("bereavement"), oldPublishedDate, + deniedPageUrl, ) should be(false) shouldAddAffiliateLinks( switchedOn = true, @@ -79,6 +86,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List("tech"), oldPublishedDate, + deniedPageUrl, ) should be(false) shouldAddAffiliateLinks( switchedOn = true, @@ -89,6 +97,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set.empty, List("tech"), oldPublishedDate, + deniedPageUrl, ) should be(true) shouldAddAffiliateLinks( switchedOn = true, @@ -99,6 +108,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set("bereavement"), List("bereavement"), oldPublishedDate, + deniedPageUrl, ) should be(false) shouldAddAffiliateLinks( switchedOn = true, @@ -109,6 +119,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set("bereavement"), List("tech"), oldPublishedDate, + deniedPageUrl, ) should be(true) shouldAddAffiliateLinks( switchedOn = true, @@ -119,6 +130,18 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { Set("bereavement"), List("tech"), newPublishedDate, + allowedPageUrl, + ) should be(true) + shouldAddAffiliateLinks( + switchedOn = true, + "film", + None, + supportedSections, + Set.empty, + Set.empty, + List.empty, + newPublishedDate, + deniedPageUrl, ) should be(false) } } diff --git a/static/src/stylesheets/module/content-garnett/_gallery.head.scss b/static/src/stylesheets/module/content-garnett/_gallery.head.scss index c8ef389043c..f7ec7df1323 100644 --- a/static/src/stylesheets/module/content-garnett/_gallery.head.scss +++ b/static/src/stylesheets/module/content-garnett/_gallery.head.scss @@ -286,3 +286,25 @@ fill: $brightness-86; } } + +.gallery__disclaimer { + color: $brightness-86; + margin-bottom: .5rem; + font-family: 'Guardian Text Sans Web', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif; + font-size: 12px; + + @include mq(desktop) { + padding-right: $gs-gutter / 2; + } + + a { + color: $brightness-86; + border-bottom: 1px solid $brightness-46; + transition: border-color .15s ease-out; + + &:hover { + border-bottom: 1px solid $brightness-97; + text-decoration: none; + } + } +}