From 0f6bb083acb42e89824829aada0c3d8b11117964 Mon Sep 17 00:00:00 2001 From: Peran Osborn Date: Fri, 9 Oct 2026 19:43:56 +0100 Subject: [PATCH 1/3] Show full height masthead for The Filter in the revamped immersive layout Immersive articles in the revamped layout (StandardLayout) always got the slim nav. Articles tagged with a Filter series tag now get the full height masthead. Adds hasFilterSeriesTag to identify Filter content by its series tags. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/layouts/StandardLayout.tsx | 6 +++- dotcom-rendering/src/lib/theFilter.test.ts | 32 ++++++++++++++++++- dotcom-rendering/src/lib/theFilter.ts | 14 ++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index aa366232619..7557b830a03 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -28,6 +28,7 @@ import { import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; +import { hasFilterSeriesTag } from '../lib/theFilter'; import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; @@ -109,6 +110,8 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isWorldCup2026 = article.tags.some((tag) => tag.id === worldCupTagId); + const isFilterSeries = hasFilterSeriesTag(article.tags); + const renderAds = canRenderAds(article); return ( @@ -149,7 +152,8 @@ export const StandardLayout = (props: WebProps | AppProps) => { } } showSlimNav={ - format.display === ArticleDisplay.Immersive || + (format.display === ArticleDisplay.Immersive && + !isFilterSeries) || isInteractive } hasPageSkinContentSelfConstrain={true} diff --git a/dotcom-rendering/src/lib/theFilter.test.ts b/dotcom-rendering/src/lib/theFilter.test.ts index bc2723ddaf4..57c39297b68 100644 --- a/dotcom-rendering/src/lib/theFilter.test.ts +++ b/dotcom-rendering/src/lib/theFilter.test.ts @@ -1,4 +1,4 @@ -import { isFilterPageId } from './theFilter'; +import { hasFilterSeriesTag, isFilterPageId } from './theFilter'; describe('isFilterPageId', () => { it('returns true for a UK Filter article pageId', () => { @@ -29,3 +29,33 @@ describe('isFilterPageId', () => { ).toBe(false); }); }); + +describe('hasFilterSeriesTag', () => { + it('returns true for the UK Filter series tag', () => { + expect( + hasFilterSeriesTag([ + { id: 'lifeandstyle/lifeandstyle' }, + { id: 'thefilter/series/the-filter' }, + ]), + ).toBe(true); + }); + + it('returns true for the US Filter series tag', () => { + expect( + hasFilterSeriesTag([{ id: 'thefilter-us/series/thefilter-us' }]), + ).toBe(true); + }); + + it('returns false when there is no Filter series tag', () => { + expect( + hasFilterSeriesTag([ + { id: 'technology/technology' }, + { id: 'tracking/commissioningdesk/the-filter' }, + ]), + ).toBe(false); + }); + + it('returns false for no tags', () => { + expect(hasFilterSeriesTag([])).toBe(false); + }); +}); diff --git a/dotcom-rendering/src/lib/theFilter.ts b/dotcom-rendering/src/lib/theFilter.ts index 202d2650cbe..8abdbf59fc0 100644 --- a/dotcom-rendering/src/lib/theFilter.ts +++ b/dotcom-rendering/src/lib/theFilter.ts @@ -1,3 +1,5 @@ +import type { TagType } from '../types/tag'; + /** * The Filter is the Guardian's shopping/product-review vertical. Its * article pageIds always start with one of these section prefixes, @@ -7,3 +9,15 @@ const FILTER_PAGE_ID_PREFIXES = ['thefilter/', 'thefilter-us/']; export const isFilterPageId = (pageId: string): boolean => FILTER_PAGE_ID_PREFIXES.some((prefix) => pageId.startsWith(prefix)); + +/** + * The Filter's series tags. Composer identifies Filter content by these, so + * prefer this over the pageId when deciding whether something is The Filter. + */ +const FILTER_SERIES_TAG_IDS = [ + 'thefilter/series/the-filter', + 'thefilter-us/series/thefilter-us', +]; + +export const hasFilterSeriesTag = (tags: Array>): boolean => + tags.some((tag) => FILTER_SERIES_TAG_IDS.includes(tag.id)); From 9cfcea8524db5d0969ac721cd729694dd9362e76 Mon Sep 17 00:00:00 2001 From: Peran Osborn Date: Fri, 9 Oct 2026 22:16:23 +0100 Subject: [PATCH 2/3] Show the Filter dateline in the immersive layout Filter articles in the immersive layout now get the same TimeDateline as the standard layout, by passing webPublicationDate to ArticleMeta. hasFilterSeriesTag becomes hasFilterTag and now also matches the Filter commissioning desk tracking tags, so ArticleMeta and StandardLayout share one check for Filter content. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/components/ArticleMeta.web.tsx | 7 ++--- .../src/layouts/ImmersiveLayout.tsx | 6 ++++ .../src/layouts/StandardLayout.tsx | 6 ++-- dotcom-rendering/src/lib/theFilter.test.ts | 28 +++++++++++++------ dotcom-rendering/src/lib/theFilter.ts | 14 ++++++---- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 12f6f93e422..8c74080a51a 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -23,6 +23,7 @@ import { import { getAudioData } from '../lib/audio-data'; import { getSoleContributor } from '../lib/byline'; import { decideMainMediaCaption } from '../lib/decide-caption'; +import { hasFilterTag } from '../lib/theFilter'; import { palette as themePalette } from '../palette'; import { hasPreferredSourceButton } from '../preferredSource'; import type { Branding as BrandingType } from '../types/branding'; @@ -339,11 +340,7 @@ export const ArticleMeta = ({ const podcast = getPodcast(tags); const rssFeedUrl = getRssFeedUrl(tags); - const isFilterArticle = tags.some( - (tag) => - tag.id === 'tracking/commissioningdesk/the-filter' || - tag.id === 'tracking/commissioningdesk/filter-us', - ); + const isFilterArticle = hasFilterTag(tags); const mediaType: | 'YouTubeVideo' diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx index e06d5d7477a..4ebd76f3397 100644 --- a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -622,6 +622,9 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => { secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={ + article.webPublicationDate + } isCommentable={ article.isCommentable } @@ -650,6 +653,9 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => { secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={ + article.webPublicationDate + } isCommentable={ article.isCommentable } diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 7557b830a03..72038af6309 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -28,7 +28,7 @@ import { import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; -import { hasFilterSeriesTag } from '../lib/theFilter'; +import { hasFilterTag } from '../lib/theFilter'; import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; @@ -110,7 +110,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isWorldCup2026 = article.tags.some((tag) => tag.id === worldCupTagId); - const isFilterSeries = hasFilterSeriesTag(article.tags); + const isFilterArticle = hasFilterTag(article.tags); const renderAds = canRenderAds(article); @@ -153,7 +153,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { } showSlimNav={ (format.display === ArticleDisplay.Immersive && - !isFilterSeries) || + !isFilterArticle) || isInteractive } hasPageSkinContentSelfConstrain={true} diff --git a/dotcom-rendering/src/lib/theFilter.test.ts b/dotcom-rendering/src/lib/theFilter.test.ts index 57c39297b68..da8a1b24496 100644 --- a/dotcom-rendering/src/lib/theFilter.test.ts +++ b/dotcom-rendering/src/lib/theFilter.test.ts @@ -1,4 +1,4 @@ -import { hasFilterSeriesTag, isFilterPageId } from './theFilter'; +import { hasFilterTag, isFilterPageId } from './theFilter'; describe('isFilterPageId', () => { it('returns true for a UK Filter article pageId', () => { @@ -30,10 +30,10 @@ describe('isFilterPageId', () => { }); }); -describe('hasFilterSeriesTag', () => { +describe('hasFilterTag', () => { it('returns true for the UK Filter series tag', () => { expect( - hasFilterSeriesTag([ + hasFilterTag([ { id: 'lifeandstyle/lifeandstyle' }, { id: 'thefilter/series/the-filter' }, ]), @@ -41,21 +41,33 @@ describe('hasFilterSeriesTag', () => { }); it('returns true for the US Filter series tag', () => { + expect(hasFilterTag([{ id: 'thefilter-us/series/thefilter-us' }])).toBe( + true, + ); + }); + + it('returns true for the UK Filter commissioning desk', () => { + expect( + hasFilterTag([{ id: 'tracking/commissioningdesk/the-filter' }]), + ).toBe(true); + }); + + it('returns true for the US Filter commissioning desk', () => { expect( - hasFilterSeriesTag([{ id: 'thefilter-us/series/thefilter-us' }]), + hasFilterTag([{ id: 'tracking/commissioningdesk/filter-us' }]), ).toBe(true); }); - it('returns false when there is no Filter series tag', () => { + it('returns false when there is no Filter series or desk tag', () => { expect( - hasFilterSeriesTag([ + hasFilterTag([ { id: 'technology/technology' }, - { id: 'tracking/commissioningdesk/the-filter' }, + { id: 'tracking/commissioningdesk/newsletters' }, ]), ).toBe(false); }); it('returns false for no tags', () => { - expect(hasFilterSeriesTag([])).toBe(false); + expect(hasFilterTag([])).toBe(false); }); }); diff --git a/dotcom-rendering/src/lib/theFilter.ts b/dotcom-rendering/src/lib/theFilter.ts index 8abdbf59fc0..86a6c780fd0 100644 --- a/dotcom-rendering/src/lib/theFilter.ts +++ b/dotcom-rendering/src/lib/theFilter.ts @@ -11,13 +11,17 @@ export const isFilterPageId = (pageId: string): boolean => FILTER_PAGE_ID_PREFIXES.some((prefix) => pageId.startsWith(prefix)); /** - * The Filter's series tags. Composer identifies Filter content by these, so - * prefer this over the pageId when deciding whether something is The Filter. + * Tags that identify Filter content: the Filter series tags, which Composer + * uses to present an article as The Filter, and the Filter commissioning desk + * tracking tags. Matching either one counts as Filter content, so prefer this + * over the pageId when deciding whether something is The Filter. */ -const FILTER_SERIES_TAG_IDS = [ +const FILTER_TAG_IDS = [ 'thefilter/series/the-filter', 'thefilter-us/series/thefilter-us', + 'tracking/commissioningdesk/the-filter', + 'tracking/commissioningdesk/filter-us', ]; -export const hasFilterSeriesTag = (tags: Array>): boolean => - tags.some((tag) => FILTER_SERIES_TAG_IDS.includes(tag.id)); +export const hasFilterTag = (tags: Array>): boolean => + tags.some((tag) => FILTER_TAG_IDS.includes(tag.id)); From 6eca94ad401b1c24ac8c5439ec172d6275f66d96 Mon Sep 17 00:00:00 2001 From: Peran Osborn Date: Tue, 13 Oct 2026 08:06:00 +0000 Subject: [PATCH 3/3] Add ArticleMeta tests for Filter dateline rendering --- .../src/components/ArticleMeta.web.test.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.test.tsx b/dotcom-rendering/src/components/ArticleMeta.web.test.tsx index da5f8da1bfd..bf8f28e997d 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.test.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.test.tsx @@ -102,4 +102,69 @@ describe('ArticleMeta', () => { container.querySelector(`.${interactiveLegacyClasses.shareIcons}`), ).toBeNull(); }); + + describe('dateline', () => { + const webPublicationDate = '2026-01-02T03:04:05.000Z'; + + const renderWithTags = ( + tags: Array<{ id: string; type: string; title: string }>, + ) => + render( + + + , + ).container; + + it('renders a time element with the raw publication date and no secondary dateline for a Filter series tag', () => { + const container = renderWithTags([ + { + id: 'thefilter/series/the-filter', + type: 'Series', + title: 'The Filter', + }, + ]); + + const time = container.querySelector('time'); + expect(time?.getAttribute('datetime')).toBe(webPublicationDate); + expect(time?.textContent).toBe('primary date line'); + expect(container.textContent).not.toContain('secondary date line'); + }); + + it('renders the standard Dateline for a non-Filter tag', () => { + const container = renderWithTags([ + { + id: 'lifeandstyle/series/observer-design', + type: 'Series', + title: 'Observer Design', + }, + ]); + + expect(container.querySelector('time')).toBeNull(); + expect(container.textContent).toContain('primary date line'); + expect(container.textContent).toContain('secondary date line'); + }); + }); });