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, + ) + )
@Html(gallery.item.trail.headline)
diff --git a/applications/app/views/package.scala b/applications/app/views/package.scala index 147d98aeacc..7b554497dc4 100644 --- a/applications/app/views/package.scala +++ b/applications/app/views/package.scala @@ -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, diff --git a/article/app/views/package.scala b/article/app/views/package.scala index cc6386a697c..05d1f6ed511 100644 --- a/article/app/views/package.scala +++ b/article/app/views/package.scala @@ -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, ), diff --git a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala index 4b1f36f8336..f1f280ab99c 100644 --- a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala @@ -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 @@ -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, diff --git a/common/app/model/dotcomrendering/DotcomRenderingUtils.scala b/common/app/model/dotcomrendering/DotcomRenderingUtils.scala index 46f1fa11eac..e240f47a632 100644 --- a/common/app/model/dotcomrendering/DotcomRenderingUtils.scala +++ b/common/app/model/dotcomrendering/DotcomRenderingUtils.scala @@ -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, @@ -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 @@ -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) } @@ -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 = @@ -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, ) } diff --git a/common/app/model/dotcomrendering/pageElements/PageElement.scala b/common/app/model/dotcomrendering/pageElements/PageElement.scala index 5b54eb315c4..a1b53999d0f 100644 --- a/common/app/model/dotcomrendering/pageElements/PageElement.scala +++ b/common/app/model/dotcomrendering/pageElements/PageElement.scala @@ -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], @@ -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 diff --git a/common/app/views/fragments/affiliateLinksDisclaimer.scala.html b/common/app/views/fragments/affiliateLinksDisclaimer.scala.html index 97aa1ecd980..2cb56a94c7e 100644 --- a/common/app/views/fragments/affiliateLinksDisclaimer.scala.html +++ b/common/app/views/fragments/affiliateLinksDisclaimer.scala.html @@ -1,34 +1,10 @@ -@(contentType: String) - - -@articleDisclaimer() = { -

- 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. -} - -@{contentType match { - case "gallery" => galleryDisclaimer() - case "article" => articleDisclaimer() - case _ => Html("") -}} +
+ \ 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; + } + } +}