From 08b94a5c194de1d6dba8564ef0c5cf97dedcc467 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:30:01 +0000 Subject: [PATCH 01/17] Audio Layout for DCAR --- .../fixtures/generated/fe-articles/Audio.ts | 1122 +++++------------ .../scripts/test-data/gen-fixtures.js | 2 +- .../src/layouts/AudioLayout.stories.tsx | 93 ++ dotcom-rendering/src/layouts/AudioLayout.tsx | 377 +++--- dotcom-rendering/src/layouts/DecideLayout.tsx | 9 + 5 files changed, 658 insertions(+), 945 deletions(-) create mode 100644 dotcom-rendering/src/layouts/AudioLayout.stories.tsx diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts index 4416802d6dc..a8aa88c3734 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts @@ -16,376 +16,40 @@ import type { FEArticle } from '../../../src/frontend/feArticle'; export const Audio: FEArticle = { version: 3, headline: - 'NSA collecting phone records of millions of Verizon customers daily', + '‘What I see in clinic is never a set of labels’: are we in danger of overdiagnosing mental illness? -podcast', standfirst: - 'Exclusive: Top secret court order requiring Verizon to hand over all call data shows scale of domestic surveillance under Obama\n
\n
\n• Read the Verizon court order in full here\n
\n• Obama administration justifies surveillance', + '

Our current approach to mental health labelling and diagnosis has brought benefits. But as a practising doctor, I am concerned that it may be doing more harm than good

By Gavin Francis. Read by Noof Ousellam

', webTitle: - 'NSA collecting phone records of millions of Verizon customers daily', + '‘What I see in clinic is never a set of labels’: are we in danger of overdiagnosing mental illness? -podcast', mainMediaElements: [ { - displayCredit: false, - _type: 'model.dotcomrendering.pageElements.ImageBlockElement', - role: 'inline', - media: { - allImages: [ - { - index: 0, - fields: { - height: '276', - width: '460', - }, - mediaType: 'Image', - mimeType: 'image/jpeg', - url: 'http://static.guim.co.uk/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg', + _type: 'model.dotcomrendering.pageElements.AudioBlockElement', + assets: [ + { + fields: { + durationMinutes: '26', + durationSeconds: '24', + source: 'The Guardian', }, - ], - }, - elementId: '38122516-c068-4549-b00e-2ace86fa1756', - imageSources: [ - { - weighting: 'inline', - srcSet: [ - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=620&quality=85&auto=format&fit=max&s=2f0303e3a6cfa368bb76b5aa1fe40ba1', - width: 620, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=ac282714e74da98178e57aed900667b3', - width: 1240, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=85&auto=format&fit=max&s=214ade1ddb558bcda2ebfb4f250a7bad', - width: 700, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=69add9eb81396bb095281d8180827dd9', - width: 1400, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=620&quality=85&auto=format&fit=max&s=2f0303e3a6cfa368bb76b5aa1fe40ba1', - width: 620, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=ac282714e74da98178e57aed900667b3', - width: 1240, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=645&quality=85&auto=format&fit=max&s=b8c14cbb95296c6ce2c47afd1c74caef', - width: 645, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=db791719b428c3726dd025373b5715c8', - width: 1290, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=465&quality=85&auto=format&fit=max&s=f4afb47d8116bd1c2f9fb126ab2d09ca', - width: 465, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=7913b6ad76e99a6811ffc359ead3f1c9', - width: 930, - }, - ], - }, - { - weighting: 'thumbnail', - srcSet: [], - }, - { - weighting: 'supporting', - srcSet: [], - }, - { - weighting: 'showcase', - srcSet: [ - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1020&quality=85&auto=format&fit=max&s=bb0557a68adb7d01d0294de4ff7c9a38', - width: 1020, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=07925e22da45fa157dd39104bea25b07', - width: 2040, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=940&quality=85&auto=format&fit=max&s=9e51b0e35b15bda73be0d1cf2a816b06', - width: 940, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=c91a29662a3b09d053d4d1fd1080db3c', - width: 1880, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=85&auto=format&fit=max&s=214ade1ddb558bcda2ebfb4f250a7bad', - width: 700, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=69add9eb81396bb095281d8180827dd9', - width: 1400, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=85&auto=format&fit=max&s=214ade1ddb558bcda2ebfb4f250a7bad', - width: 700, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=69add9eb81396bb095281d8180827dd9', - width: 1400, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=660&quality=85&auto=format&fit=max&s=f6f422301d42da7f16fdf0099e5d2abb', - width: 660, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=cd24eea3ca1f24fdeac108251502e520', - width: 1320, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=645&quality=85&auto=format&fit=max&s=b8c14cbb95296c6ce2c47afd1c74caef', - width: 645, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=db791719b428c3726dd025373b5715c8', - width: 1290, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=465&quality=85&auto=format&fit=max&s=f4afb47d8116bd1c2f9fb126ab2d09ca', - width: 465, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=7913b6ad76e99a6811ffc359ead3f1c9', - width: 930, - }, - ], - }, - { - weighting: 'halfwidth', - srcSet: [], - }, - { - weighting: 'immersive', - srcSet: [ - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1900&quality=85&auto=format&fit=max&s=23200f4fbf312d4cc329b375882540e7', - width: 1900, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=5314e4ad4795ffd3aaf1a5c8cfd97be4', - width: 3800, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1300&quality=85&auto=format&fit=max&s=ce24ae26bde4f0d84db39460758409d1', - width: 1300, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=0f9d8d9013a7e589b772d94a3626c6bb', - width: 2600, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1140&quality=85&auto=format&fit=max&s=8501c0e4d7abeb6dbc054c78276f3d0e', - width: 1140, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=3267023b74d0d6b9076ece455c939b03', - width: 2280, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=980&quality=85&auto=format&fit=max&s=e13cdce4024f0740056c3ca1a00944f1', - width: 980, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=7e73c17e8b531b5470a4f312d76851df', - width: 1960, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=740&quality=85&auto=format&fit=max&s=3d0f43f5906111bd1d8239acdcf67a9c', - width: 740, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=4d1e34d5a6d635b6fec68eb0aabeb97c', - width: 1480, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=660&quality=85&auto=format&fit=max&s=f6f422301d42da7f16fdf0099e5d2abb', - width: 660, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=cd24eea3ca1f24fdeac108251502e520', - width: 1320, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=480&quality=85&auto=format&fit=max&s=a9b086c50aea4bdc186c8e78983a10b9', - width: 480, - }, - { - src: 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=3db6a06f16d14b55a9c41b96fb84a2aa', - width: 960, - }, - ], + url: 'https://audio.guim.co.uk/2026/02/27-42462-gdn.alr.060326.NA_GAVIN_FRANCIS_MENTALILLNESS.mp3', + mimeType: 'audio/mpeg', }, ], - data: { - alt: 'Phone records data', - caption: - 'Under the terms of the order, the numbers of both parties on a call are handed over, as is location data and the time and duration of all calls. Photograph: Matt Rourke/AP', - credit: 'Photograph: Matt Rourke/AP', - }, + id: 'gu-audio-69a065c68f082be2de6a7ce1', + elementId: '983b569d-9dfb-4c30-b3b3-0fafdb75a79e', }, ], - main: '
Phone records data
Under the terms of the order, the numbers of both parties on a call are handed over, as is location data and the time and duration of all calls. Photograph: Matt Rourke/AP Photograph: Matt Rourke/AP
', + main: '', filterKeyEvents: false, keyEvents: [], blocks: [ { - id: 'ed4dfa13-125b-4c94-8b82-cc1e2bbb0553', + id: '69a0665b8f082be2de6a7cf5', elements: [ { _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

The National Security Agency is currently collecting the telephone records of millions of US customers of Verizon, one of America's largest telecoms providers, under a top secret court order issued in April.

", - elementId: '3e83c465-58b8-40e9-838f-7c1217781cee', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The order, a copy of which has been obtained by the Guardian, requires Verizon on an "ongoing, daily basis" to give the NSA information on all telephone calls in its systems, both within the US and between the US and other countries.

', - elementId: 'c84dded0-6a51-4d05-915e-b32a527c27e7', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The document shows for the first time that under the Obama administration the communication records of millions of US citizens are being collected indiscriminately and in bulk – regardless of whether they are suspected of any wrongdoing.

', - elementId: 'a6c47079-8e49-4fa1-87b4-cddc15ef243d', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The secret Foreign Intelligence Surveillance Court (Fisa) granted the order to the FBI on April 25, giving the government unlimited authority to obtain the data for a specified three-month period ending on July 19.

', - elementId: '028d4c5b-c417-434d-8113-1920ff2f3a3d', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

Under the terms of the blanket order, the numbers of both parties on a call are handed over, as is location data, call duration, unique identifiers, and the time and duration of all calls. The contents of the conversation itself are not covered.

', - elementId: 'fe7fb568-9962-419f-ab0e-cb47be19e1b8', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

The disclosure is likely to reignite longstanding debates in the US over the proper extent of the government's domestic spying powers.

", - elementId: 'aa4c82e2-3fed-4c51-9b9d-2ad1744202e9', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

Under the Bush administration, officials in security agencies had disclosed to reporters the large-scale collection of call records data by the NSA, but this is the first time significant and top-secret documents have revealed the continuation of the practice on a massive scale under President Obama.

', - elementId: '314f0233-8197-476f-a608-7682d324970e', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The unlimited nature of the records being handed over to the NSA is extremely unusual. Fisa court orders typically direct the production of records pertaining to a specific named target who is suspected of being an agent of a terrorist group or foreign state, or a finite set of individually named targets.

', - elementId: 'a96ca93b-46e7-4a1b-8643-64819c756500', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The Guardian approached the National Security Agency, the White House and the Department of Justice for comment in advance of publication on Wednesday. All declined. The agencies were also offered the opportunity to raise specific security concerns regarding the publication of the court order.

', - elementId: '461c7c71-b9d4-4e6c-a4c4-db28e2d562cd', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

The court order expressly bars Verizon from disclosing to the public either the existence of the FBI's request for its customers' records, or the court order itself.

", - elementId: '91388831-d1b0-4a33-b695-f11c63fdbdd1', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

"We decline comment," said Ed McFadden, a Washington-based Verizon spokesman.

', - elementId: 'b74f2639-82cd-4f5d-93e0-86387ef017c8', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The order, signed by Judge Roger Vinson, compels Verizon to produce to the NSA electronic copies of "all call detail records or \'telephony metadata\' created by Verizon for communications between the United States and abroad" or "wholly within the United States, including local telephone calls".

', - elementId: '63c47275-b0a0-4053-bd87-64e98af1f616', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The order directs Verizon to "continue production on an ongoing daily basis thereafter for the duration of this order". It specifies that the records to be produced include "session identifying information", such as "originating and terminating number", the duration of each call, telephone calling card numbers, trunk identifiers, International Mobile Subscriber Identity (IMSI) number, and "comprehensive communication routing information".

', - elementId: '9f807456-9f6e-490d-a653-28f8c2cfe1d7', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The information is classed as "metadata", or transactional information, rather than communications, and so does not require individual warrants to access. The document also specifies that such "metadata" is not limited to the aforementioned items. A 2005 court ruling judged that cell site location data – the nearest cell tower a phone was connected to – was also transactional data, and so could potentially fall under the scope of the order.

', - elementId: '0953c1ff-dd01-4eb3-97cf-89cf9ce8ef4d', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

While the order itself does not include either the contents of messages or the personal information of the subscriber of any particular cell number, its collection would allow the NSA to build easily a comprehensive picture of who any individual contacted, how and when, and possibly from where, retrospectively.

', - elementId: '21a6e5ec-cb6d-4bfa-b2f3-91770f08f3e2', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

It is not known whether Verizon is the only cell-phone provider to be targeted with such an order, although previous reporting has suggested the NSA has collected cell records from all major mobile networks. It is also unclear from the leaked document whether the three-month order was a one-off, or the latest in a series of similar orders.

', - elementId: 'd2febc1e-6698-41ee-929c-414e59621a25', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

The court order appears to explain the numerous cryptic public warnings by two US senators, Ron Wyden and Mark Udall, about the scope of the Obama administration's surveillance activities.

", - elementId: 'ebb464e1-a280-4a43-83a7-8842020f7472', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

For roughly two years, the two Democrats have been stridently advising the public that the US government is relying on "secret legal interpretations" to claim surveillance powers so broad that the American public would be "stunned" to learn of the kind of domestic spying being conducted.

', - elementId: '534a2ee1-0e39-4f54-80f8-bb3b388fb282', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

Because those activities are classified, the senators, both members of the Senate intelligence committee, have been prevented from specifying which domestic surveillance programs they find so alarming. But the information they have been able to disclose in their public warnings perfectly tracks both the specific law cited by the April 25 court order as well as the vast scope of record-gathering it authorized.

', - elementId: '659821fa-b2a0-40a4-8382-a2f87d90737c', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

Julian Sanchez, a surveillance expert with the Cato Institute, explained: \"We've certainly seen the government increasingly strain the bounds of 'relevance' to collect large numbers of records at once — everyone at one or two degrees of separation from a target — but vacuuming all metadata up indiscriminately would be an extraordinary repudiation of any pretence of constraint or particularized suspicion.\" The April order requested by the FBI and NSA does precisely that.

", - elementId: '69b2a72c-b1a6-4aaf-ac95-f296b73c04f4', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The law on which the order explicitly relies is the so-called "business records" provision of the Patriot Act, 50 USC section 1861. That is the provision which Wyden and Udall have repeatedly cited when warning the public of what they believe is the Obama administration\'s extreme interpretation of the law to engage in excessive domestic surveillance.

', - elementId: '917ef1fd-2257-426d-ba9c-29a877d18484', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

In a letter to attorney general Eric Holder last year, they argued that "there is now a significant gap between what most Americans think the law allows and what the government secretly claims the law allows."

', - elementId: '1a2d7356-7d1b-4d85-943d-81209415cfd8', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

"We believe," they wrote, "that most Americans would be stunned to learn the details of how these secret court opinions have interpreted" the "business records" provision of the Patriot Act.

', - elementId: '2904e1ce-de96-4b58-8aec-9b6618e1e440', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

Privacy advocates have long warned that allowing the government to collect and store unlimited "metadata" is a highly invasive form of surveillance of citizens\' communications activities. Those records enable the government to know the identity of every person with whom an individual communicates electronically, how long they spoke, and their location at the time of the communication.

', - elementId: '986f6db4-a3e4-4770-a0d1-34479bb3a8c5', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

Such metadata is what the US government has long attempted to obtain in order to discover an individual's network of associations and communication patterns. The request for the bulk collection of all Verizon domestic telephone records indicates that the agency is continuing some version of the data-mining program begun by the Bush administration in the immediate aftermath of the 9/11 attack.

", - elementId: 'e08682c0-bb7a-4b83-9ad1-958a6ca84f71', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

The NSA, as part of a program secretly authorized by President Bush on 4 October 2001, implemented a bulk collection program of domestic telephone, internet and email records. A furore erupted in 2006 when USA Today reported that the NSA had "been secretly collecting the phone call records of tens of millions of Americans, using data provided by AT&T, Verizon and BellSouth" and was "using the data to analyze calling patterns in an effort to detect terrorist activity." Until now, there has been no indication that the Obama administration implemented a similar program.

', - elementId: '600497eb-c849-4b70-8af1-a8d2770a28cd', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: "

These recent events reflect how profoundly the NSA's mission has transformed from an agency exclusively devoted to foreign intelligence gathering, into one that focuses increasingly on domestic communications. A 30-year employee of the NSA, William Binney, resigned from the agency shortly after 9/11 in protest at the agency's focus on domestic activities.

", - elementId: '9ffbda8e-8fb4-4841-938b-91bb19e46fe7', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

In the mid-1970s, Congress, for the first time, investigated the surveillance activities of the US government. Back then, the mandate of the NSA was that it would never direct its surveillance apparatus domestically.

', - elementId: '722b38a0-22c5-4eb6-ace0-01017863d128', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

At the conclusion of that investigation, Frank Church, the Democratic senator from Idaho who chaired the investigative committee, warned: "The NSA\'s capability at any time could be turned around on the American people, and no American would have any privacy left, such is the capability to monitor everything: telephone conversations, telegrams, it doesn\'t matter."

', - elementId: '6ef7b160-8070-412b-8c27-f4654498f2f2', - }, - { - _type: 'model.dotcomrendering.pageElements.TextBlockElement', - html: '

Additional reporting by Ewen MacAskill and Spencer Ackerman

', - elementId: '4ae92bef-67f8-47a1-b56c-790f9c891867', + html: '', + elementId: '4b2a79d5-43cb-4e17-a2df-3121fdd58cbc', }, ], attributes: { @@ -393,193 +57,144 @@ export const Audio: FEArticle = { keyEvent: false, summary: false, }, - blockCreatedOn: 1452843129000, - blockCreatedOnDisplay: '07.32 GMT', - blockLastUpdated: 1452843129000, - blockLastUpdatedDisplay: '07.32 GMT', + blockCreatedOn: 1772773211000, + blockCreatedOnDisplay: '05.00 GMT', + blockLastUpdated: 1772119661000, + blockLastUpdatedDisplay: '15.27 GMT', + blockFirstPublished: 1772773211000, + blockFirstPublishedDisplay: '05.00 GMT', + blockFirstPublishedDisplayNoTimezone: '05.00', contributors: [], - primaryDateLine: 'Thu 6 Jun 2013 11.05 BST', - secondaryDateLine: 'First published on Thu 6 Jun 2013 11.05 BST', + primaryDateLine: 'Fri 6 Mar 2026 05.00 GMT', + secondaryDateLine: 'Last modified on Fri 6 Mar 2026 05.00 GMT', }, ], author: { - byline: 'Glenn Greenwald', + byline: 'Written by Gavin Francis and read by Noof Ousellam. Produced by Nicola Alexandrou. The executive producer was Ellie Bury', + twitterHandle: 'gavinfranc', }, - byline: 'Glenn Greenwald', - webPublicationDate: '2013-06-06T10:05:00.000Z', - webPublicationDateDeprecated: '2013-06-06T10:05:00.000Z', - webPublicationDateDisplay: 'Thu 6 Jun 2013 11.05 BST', + byline: 'Written by Gavin Francis and read by Noof Ousellam. Produced by Nicola Alexandrou. The executive producer was Ellie Bury', + webPublicationDate: '2026-03-06T05:00:11.000Z', + webPublicationDateDeprecated: '2026-03-06T05:00:11.000Z', + webPublicationDateDisplay: 'Fri 6 Mar 2026 05.00 GMT', webPublicationSecondaryDateDisplay: - 'First published on Thu 6 Jun 2013 11.05 BST', + 'Last modified on Fri 6 Mar 2026 05.00 GMT', editionLongForm: 'UK edition', editionId: 'UK', - pageId: 'world/2013/jun/06/nsa-phone-records-verizon-court-order', + pageId: 'news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', canonicalUrl: - 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', format: { - design: 'ArticleDesign', + design: 'AudioDesign', theme: 'NewsPillar', display: 'StandardDisplay', }, - designType: 'Article', + designType: 'Media', tags: [ { - id: 'us-news/us-national-security', - type: 'Keyword', - title: 'US national security', - }, - { - id: 'us-news/us-politics', - type: 'Keyword', - title: 'US politics', - }, - { - id: 'world/privacy', - type: 'Keyword', - title: 'Privacy', - }, - { - id: 'business/telecoms', - type: 'Keyword', - title: 'Telecommunications industry', - }, - { - id: 'technology/telecoms', - type: 'Keyword', - title: 'Telecoms', + id: 'news/series/the-audio-long-read', + type: 'Series', + title: 'The Audio Long Read', + podcast: { + subscriptionUrl: + 'https://itunes.apple.com/gb/podcast/the-guardian-long-read/id587347784?mt=2', + spotifyUrl: + 'https://open.spotify.com/show/0jG1HXr3tGoGorW1ieytRS', + image: 'https://uploads.guim.co.uk/2021/01/22/AudioLongReadJan2021.jpg', + }, }, { - id: 'business/verizon-communications', - type: 'Keyword', - title: 'Verizon Communications', + id: 'news/series/the-long-read', + type: 'Series', + title: 'The long read', }, { - id: 'technology/data-protection', + id: 'society/mental-health', type: 'Keyword', - title: 'Data protection', + title: 'Mental health', }, { - id: 'technology/technology', + id: 'society/health', type: 'Keyword', - title: 'Technology', + title: 'Health', }, { - id: 'business/business', + id: 'society/society', type: 'Keyword', - title: 'Business', + title: 'Society', }, { - id: 'world/world', - type: 'Keyword', - title: 'World news', + id: 'type/audio', + type: 'Type', + title: 'Audio', }, { - id: 'tone/news', + id: 'tone/features', type: 'Tone', - title: 'News', - }, - { - id: 'us-news/us-news', - type: 'Keyword', - title: 'US news', - }, - { - id: 'commentisfree/series/glenn-greenwald-security-liberty', - type: 'Series', - title: 'Glenn Greenwald on security and liberty', - }, - { - id: 'us-news/nsa', - type: 'Keyword', - title: 'NSA', + title: 'Features', }, { - id: 'us-news/the-nsa-files', - type: 'Keyword', - title: 'The NSA files', + id: 'type/podcast', + type: 'Type', + title: 'Podcast', }, { - id: 'type/article', - type: 'Type', - title: 'Article', + id: 'profile/gavin-francis', + type: 'Contributor', + title: 'Gavin Francis', + twitterHandle: 'gavinfranc', }, { - id: 'profile/glenn-greenwald', + id: 'profile/nicola-alexandrou', type: 'Contributor', - title: 'Glenn Greenwald', - bylineImageUrl: - 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/pictures/2012/8/16/1345126789538/Gleen_Greenwald-140.jpg?width=300&quality=85&auto=format&fit=max&s=e67b2ebc5cf82a3ed2ea1d3cb2f0b268', + title: 'Nicola Alexandrou', }, { - id: 'publication/theguardian', - type: 'Publication', - title: 'The Guardian', + id: 'profile/ellie-bury', + type: 'Contributor', + title: 'Ellie Bury', }, { - id: 'theguardian/mainsection', - type: 'NewspaperBook', - title: 'Main section', + id: 'tracking/commissioningdesk/uk-audio', + type: 'Tracking', + title: 'UK Audio', }, { - id: 'theguardian/mainsection/topstories', - type: 'NewspaperBookSection', - title: 'Top stories', + id: 'tracking/commissioningdesk/long-read', + type: 'Tracking', + title: 'UK Long Read', }, ], pillar: 'news', isLegacyInteractive: false, isImmersive: false, - sectionLabel: 'US national security', - sectionUrl: 'us-news/us-national-security', - sectionName: 'us-news', + sectionLabel: 'Mental health', + sectionUrl: 'society/mental-health', + sectionName: 'news', subMetaSectionLinks: [ { - url: '/us-news/us-national-security', - title: 'US national security', + url: '/society/mental-health', + title: 'Mental health', }, { - url: '/commentisfree/series/glenn-greenwald-security-liberty', - title: 'Glenn Greenwald on security and liberty', + url: '/news/series/the-audio-long-read', + title: 'The Audio Long Read', }, ], subMetaKeywordLinks: [ { - url: '/us-news/us-politics', - title: 'US politics', - }, - { - url: '/world/privacy', - title: 'Privacy', - }, - { - url: '/business/telecoms', - title: 'Telecommunications industry', - }, - { - url: '/technology/telecoms', - title: 'Telecoms', - }, - { - url: '/business/verizon-communications', - title: 'Verizon Communications', - }, - { - url: '/technology/data-protection', - title: 'Data protection', - }, - { - url: '/tone/news', - title: 'news', + url: '/society/health', + title: 'Health', }, ], shouldHideAds: false, isAdFreeUser: false, - webURL: 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + webURL: 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', linkedData: [ { '@type': 'NewsArticle', '@context': 'https://schema.org', - '@id': 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + '@id': 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', publisher: { '@type': 'Organization', '@context': 'https://schema.org', @@ -605,58 +220,65 @@ export const Audio: FEArticle = { productID: 'theguardian.com:basic', }, image: [ - 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctYWdlLTIwMTMucG5n&enable=upscale&s=cc877710aa4399e5295407faaefca3b2', - 'https://assets.guim.co.uk/images/eada8aa27c12fe2d5afa3a89d3fbae0d/fallback-logo.png', - 'https://assets.guim.co.uk/images/eada8aa27c12fe2d5afa3a89d3fbae0d/fallback-logo.png', - 'https://assets.guim.co.uk/images/eada8aa27c12fe2d5afa3a89d3fbae0d/fallback-logo.png', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctZGVmYXVsdC5wbmc&enable=upscale&s=fb38271a003654a1f9f9f571ae2bae5a', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=9dbedb17a7e864db69dafbd83a250921', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=181f1359f2866b463c920b03d79fff9d', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&quality=85&auto=format&fit=max&s=5a969034ad587991d9adcb755a70101d', ], author: [ { '@type': 'Person', - name: 'Glenn Greenwald', - sameAs: 'https://www.theguardian.com/profile/glenn-greenwald', + name: 'Gavin Francis', + sameAs: 'https://www.theguardian.com/profile/gavin-francis', + }, + { + '@type': 'Person', + name: 'Nicola Alexandrou', + sameAs: 'https://www.theguardian.com/profile/nicola-alexandrou', + }, + { + '@type': 'Person', + name: 'Ellie Bury', + sameAs: 'https://www.theguardian.com/profile/ellie-bury', }, ], - datePublished: '2013-06-06T10:05:00.000Z', + datePublished: '2026-03-06T05:00:11.000Z', headline: - 'NSA collecting phone records of millions of Verizon customers daily', - dateModified: '2019-05-01T17:16:49.000Z', + '‘What I see in clinic is never a set of labels’: are we in danger of overdiagnosing mental illness? -podcast', + dateModified: '2026-03-06T05:00:11.000Z', mainEntityOfPage: - 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { '@type': 'WebPage', '@context': 'https://schema.org', - '@id': 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + '@id': 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', potentialAction: { '@type': 'ViewAction', - target: 'android-app://com.guardian/https/www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + target: 'android-app://com.guardian/https/www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, }, ], openGraphData: { 'og:url': - 'http://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', - 'article:author': 'https://www.theguardian.com/profile/glenn-greenwald', + 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', 'og:image:width': '1200', 'og:image': - 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctYWdlLTIwMTMucG5n&enable=upscale&s=cc877710aa4399e5295407faaefca3b2', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctZGVmYXVsdC5wbmc&enable=upscale&s=fb38271a003654a1f9f9f571ae2bae5a', 'al:ios:url': - 'gnmguardian://world/2013/jun/06/nsa-phone-records-verizon-court-order?contenttype=Article&source=applinks', - 'article:publisher': 'https://www.facebook.com/theguardian', + 'gnmguardian://news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast?contenttype=Article&source=applinks', 'og:title': - 'NSA collecting phone records of millions of Verizon customers daily', + '‘What I see in clinic is never a set of labels’: are we in danger of overdiagnosing mental illness? -podcast', 'fb:app_id': '180444840287', - 'article:modified_time': '2019-05-01T17:16:49.000Z', - 'og:image:height': '720', + 'article:modified_time': '2026-03-06T05:00:11.000Z', + 'og:image:height': '960', 'og:description': - 'Exclusive: Top secret court order requiring Verizon to hand over all call data shows scale of domestic surveillance under Obama administration', + 'Our current approach to mental health labelling and diagnosis has brought benefits. But as a practising doctor, I am concerned that it may be doing more harm than goodBy Gavin Francis. Read by Noof Ousellam', 'og:type': 'article', 'al:ios:app_store_id': '409128287', - 'article:section': 'US news', - 'article:published_time': '2013-06-06T10:05:00.000Z', - 'article:tag': - 'US national security,US politics,Privacy,Telecommunications industry,Telecoms,Verizon Communications,Data protection,Technology,Business,World news,US news,NSA,The NSA files', + 'article:section': 'News', + 'article:published_time': '2026-03-06T05:00:11.000Z', + 'article:tag': 'Mental health,Health,Society', 'al:ios:app_name': 'The Guardian', 'og:site_name': 'the Guardian', }, @@ -666,17 +288,18 @@ export const Audio: FEArticle = { 'twitter:app:name:ipad': 'The Guardian', 'twitter:card': 'summary_large_image', 'twitter:app:name:iphone': 'The Guardian', + 'twitter:creator': '@gavinfranc', 'twitter:app:id:ipad': '409128287', 'twitter:app:id:googleplay': 'com.guardian', 'twitter:app:url:googleplay': - 'guardian://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + 'guardian://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', 'twitter:app:url:iphone': - 'gnmguardian://world/2013/jun/06/nsa-phone-records-verizon-court-order?contenttype=Article&source=twitter', + 'gnmguardian://news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast?contenttype=Article&source=twitter', 'twitter:image': - 'https://i.guim.co.uk/img/static/sys-images/Guardian/Pix/audio/video/2013/6/5/1370461050798/Phone-records-data-010.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctYWdlLTIwMTMucG5n&s=dd2482dfcbf026416320a4ad45e45237', + 'https://i.guim.co.uk/img/media/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctZGVmYXVsdC5wbmc&s=1d3f84aca5d5bd7ea6b85329ad238432', 'twitter:site': '@guardian', 'twitter:app:url:ipad': - 'gnmguardian://world/2013/jun/06/nsa-phone-records-verizon-court-order?contenttype=Article&source=twitter', + 'gnmguardian://news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast?contenttype=Article&source=twitter', }, config: { references: [ @@ -844,11 +467,11 @@ export const Audio: FEArticle = { discussionD2Uid: 'zHoBy6HNKsk', }, guardianBaseURL: 'https://www.theguardian.com', - contentType: 'Article', + contentType: 'Audio', hasRelated: true, hasStoryPackage: false, beaconURL: '//phar.gu-web.net', - isCommentable: true, + isCommentable: false, commercialProperties: { US: { adTargeting: [ @@ -857,53 +480,40 @@ export const Audio: FEArticle = { value: ['0'], }, { - name: 'ct', - value: 'article', + name: 'edition', + value: 'us', + }, + { + name: 'url', + value: '/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'tn', - value: ['news'], + value: ['features'], }, { name: 'co', - value: ['glenn-greenwald'], + value: ['gavin-francis', 'ellie-bury', 'nicola-alexandrou'], }, { - name: 'k', - value: [ - 'business', - 'us-national-security', - 'world', - 'data-protection', - 'us-politics', - 'technology', - 'nsa', - 'telecoms', - 'privacy', - 'the-nsa-files', - 'verizon-communications', - 'us-news', - ], + name: 'ct', + value: 'audio', }, { - name: 'url', - value: '/world/2013/jun/06/nsa-phone-records-verizon-court-order', + name: 'se', + value: ['the-long-read', 'the-audio-long-read'], }, { - name: 'p', - value: 'ng', + name: 'k', + value: ['health', 'society', 'mental-health'], }, { name: 'sh', - value: 'https://www.theguardian.com/p/3gc62', + value: 'https://www.theguardian.com/p/x4f2pp', }, { - name: 'se', - value: ['glenn-greenwald-security-liberty'], - }, - { - name: 'edition', - value: 'us', + name: 'p', + value: 'ng', }, ], }, @@ -918,49 +528,36 @@ export const Audio: FEArticle = { value: 'au', }, { - name: 'se', - value: ['glenn-greenwald-security-liberty'], - }, - { - name: 'ct', - value: 'article', + name: 'url', + value: '/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'tn', - value: ['news'], + value: ['features'], }, { name: 'co', - value: ['glenn-greenwald'], + value: ['gavin-francis', 'ellie-bury', 'nicola-alexandrou'], }, { - name: 'k', - value: [ - 'business', - 'us-national-security', - 'world', - 'data-protection', - 'us-politics', - 'technology', - 'nsa', - 'telecoms', - 'privacy', - 'the-nsa-files', - 'verizon-communications', - 'us-news', - ], + name: 'ct', + value: 'audio', }, { - name: 'url', - value: '/world/2013/jun/06/nsa-phone-records-verizon-court-order', + name: 'se', + value: ['the-long-read', 'the-audio-long-read'], }, { - name: 'p', - value: 'ng', + name: 'k', + value: ['health', 'society', 'mental-health'], }, { name: 'sh', - value: 'https://www.theguardian.com/p/3gc62', + value: 'https://www.theguardian.com/p/x4f2pp', + }, + { + name: 'p', + value: 'ng', }, ], }, @@ -970,54 +567,41 @@ export const Audio: FEArticle = { name: 'su', value: ['0'], }, - { - name: 'se', - value: ['glenn-greenwald-security-liberty'], - }, { name: 'edition', value: 'uk', }, { - name: 'ct', - value: 'article', + name: 'url', + value: '/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'tn', - value: ['news'], + value: ['features'], }, { name: 'co', - value: ['glenn-greenwald'], + value: ['gavin-francis', 'ellie-bury', 'nicola-alexandrou'], }, { - name: 'k', - value: [ - 'business', - 'us-national-security', - 'world', - 'data-protection', - 'us-politics', - 'technology', - 'nsa', - 'telecoms', - 'privacy', - 'the-nsa-files', - 'verizon-communications', - 'us-news', - ], + name: 'ct', + value: 'audio', }, { - name: 'url', - value: '/world/2013/jun/06/nsa-phone-records-verizon-court-order', + name: 'se', + value: ['the-long-read', 'the-audio-long-read'], }, { - name: 'p', - value: 'ng', + name: 'k', + value: ['health', 'society', 'mental-health'], }, { name: 'sh', - value: 'https://www.theguardian.com/p/3gc62', + value: 'https://www.theguardian.com/p/x4f2pp', + }, + { + name: 'p', + value: 'ng', }, ], }, @@ -1028,53 +612,40 @@ export const Audio: FEArticle = { value: ['0'], }, { - name: 'se', - value: ['glenn-greenwald-security-liberty'], - }, - { - name: 'ct', - value: 'article', + name: 'url', + value: '/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'tn', - value: ['news'], + value: ['features'], }, { name: 'co', - value: ['glenn-greenwald'], + value: ['gavin-francis', 'ellie-bury', 'nicola-alexandrou'], }, { - name: 'k', - value: [ - 'business', - 'us-national-security', - 'world', - 'data-protection', - 'us-politics', - 'technology', - 'nsa', - 'telecoms', - 'privacy', - 'the-nsa-files', - 'verizon-communications', - 'us-news', - ], + name: 'ct', + value: 'audio', }, { - name: 'url', - value: '/world/2013/jun/06/nsa-phone-records-verizon-court-order', + name: 'se', + value: ['the-long-read', 'the-audio-long-read'], }, { - name: 'edition', - value: 'int', + name: 'k', + value: ['health', 'society', 'mental-health'], }, { name: 'p', value: 'ng', }, + { + name: 'edition', + value: 'int', + }, { name: 'sh', - value: 'https://www.theguardian.com/p/3gc62', + value: 'https://www.theguardian.com/p/x4f2pp', }, ], }, @@ -1085,49 +656,36 @@ export const Audio: FEArticle = { value: ['0'], }, { - name: 'se', - value: ['glenn-greenwald-security-liberty'], - }, - { - name: 'ct', - value: 'article', + name: 'url', + value: '/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'tn', - value: ['news'], + value: ['features'], }, { name: 'co', - value: ['glenn-greenwald'], + value: ['gavin-francis', 'ellie-bury', 'nicola-alexandrou'], }, { - name: 'url', - value: '/world/2013/jun/06/nsa-phone-records-verizon-court-order', + name: 'ct', + value: 'audio', }, { - name: 'p', - value: 'ng', + name: 'k', + value: ['health', 'society', 'mental-health'], }, { name: 'sh', - value: 'https://www.theguardian.com/p/3gc62', + value: 'https://www.theguardian.com/p/x4f2pp', }, { - name: 'k', - value: [ - 'business', - 'us-national-security', - 'world', - 'data-protection', - 'us-politics', - 'technology', - 'nsa', - 'telecoms', - 'privacy', - 'the-nsa-files', - 'verizon-communications', - 'us-news', - ], + name: 'p', + value: 'ng', + }, + { + name: 'se', + value: ['the-long-read', 'the-audio-long-read'], }, { name: 'edition', @@ -1145,10 +703,73 @@ export const Audio: FEArticle = { isPreview: false, isSensitive: false, }, + audioArticleImage: { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 1, + fields: { + displayCredit: 'true', + isMaster: 'true', + altText: 'Illustration: Anais Mims/Guardian Design', + mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', + width: '3750', + source: 'Getty / Guardian Design', + photographer: 'Anaïs Mims', + height: '3000', + credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', + }, + mediaType: 'Image', + url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg', + }, + { + index: 1, + fields: { + displayCredit: 'true', + altText: 'Illustration: Anais Mims/Guardian Design', + mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', + width: '3750', + source: 'Getty / Guardian Design', + photographer: 'Anaïs Mims', + height: '3000', + credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', + }, + mediaType: 'Image', + url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/3750.jpg', + }, + { + index: 1, + fields: { + displayCredit: 'true', + altText: 'Illustration: Anais Mims/Guardian Design', + mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', + width: '500', + source: 'Getty / Guardian Design', + photographer: 'Anaïs Mims', + height: '400', + credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', + }, + mediaType: 'Image', + url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/500.jpg', + }, + ], + }, + elementId: '8f1d3dc4-f0a6-4594-9a70-83489d6a8b7d', + imageSources: [], + data: { + copyright: '', + alt: 'Illustration: Anais Mims/Guardian Design', + caption: '', + credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', + }, + }, trailText: - '

Exclusive: Top secret court order requiring Verizon to hand over all call data shows scale of domestic surveillance under Obama administration

', + 'Our current approach to mental health labelling and diagnosis has brought benefits. But as a practising doctor, I am concerned that it may be doing more harm than good

By Gavin Francis. Read by Noof Ousellam', nav: { - currentUrl: '/us-news', + currentUrl: '/news', pillars: [ { title: 'News', @@ -1308,11 +929,6 @@ export const Audio: FEArticle = { url: '/football/teams', longTitle: 'football/teams', }, - { - title: 'Euro 2025', - url: '/football/women-s-euro-2025', - longTitle: 'football/women-s-euro-2025', - }, ], }, { @@ -1453,6 +1069,10 @@ export const Audio: FEArticle = { longTitle: 'Sport home', iconName: 'home', children: [ + { + title: 'Winter Paralympics', + url: '/sport/winter-paralympics', + }, { title: 'Football', url: '/football', @@ -1487,11 +1107,6 @@ export const Audio: FEArticle = { url: '/football/teams', longTitle: 'football/teams', }, - { - title: 'Euro 2025', - url: '/football/women-s-euro-2025', - longTitle: 'football/women-s-euro-2025', - }, ], }, { @@ -1740,6 +1355,10 @@ export const Audio: FEArticle = { title: 'Sunday quick', url: '/crosswords/series/sunday-quick', }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, { title: 'Quick cryptic', url: '/crosswords/series/quick-cryptic', @@ -1821,95 +1440,6 @@ export const Audio: FEArticle = { url: 'https://licensing.theguardian.com/', }, ], - currentNavLinkTitle: 'US news', - currentPillarTitle: 'News', - subNavSections: { - parent: { - title: 'World', - url: '/world', - longTitle: 'World news', - children: [ - { - title: 'Europe', - url: '/world/europe-news', - }, - { - title: 'US news', - url: '/us-news', - longTitle: 'US news', - }, - { - title: 'Americas', - url: '/world/americas', - }, - { - title: 'Asia', - url: '/world/asia', - }, - { - title: 'Australia', - url: '/australia-news', - longTitle: 'Australia news', - }, - { - title: 'Middle East', - url: '/world/middleeast', - }, - { - title: 'Africa', - url: '/world/africa', - }, - { - title: 'Inequality', - url: '/inequality', - }, - { - title: 'Global development', - url: '/global-development', - }, - ], - }, - links: [ - { - title: 'Europe', - url: '/world/europe-news', - }, - { - title: 'US news', - url: '/us-news', - longTitle: 'US news', - }, - { - title: 'Americas', - url: '/world/americas', - }, - { - title: 'Asia', - url: '/world/asia', - }, - { - title: 'Australia', - url: '/australia-news', - longTitle: 'Australia news', - }, - { - title: 'Middle East', - url: '/world/middleeast', - }, - { - title: 'Africa', - url: '/world/africa', - }, - { - title: 'Inequality', - url: '/inequality', - }, - { - title: 'Global development', - url: '/global-development', - }, - ], - }, readerRevenueLinks: { header: { contribute: @@ -1963,7 +1493,7 @@ export const Audio: FEArticle = { }, }, }, - showBottomSocialButtons: true, + showBottomSocialButtons: false, pageFooter: { footerLinks: [ [ @@ -1986,15 +1516,21 @@ export const Audio: FEArticle = { extraClasses: '', }, { - text: 'SecureDrop', - url: 'https://www.theguardian.com/securedrop', - dataLinkName: 'securedrop', + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', extraClasses: '', }, { - text: 'Work for us', - url: 'https://workforus.theguardian.com', - dataLinkName: 'uk : footer : work for us', + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', extraClasses: '', }, { @@ -2010,15 +1546,21 @@ export const Audio: FEArticle = { extraClasses: '', }, { - text: 'Terms & conditions', - url: '/help/terms-of-service', - dataLinkName: 'terms', + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2025/09/05/Modern_Slavery_Statement_2025.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', extraClasses: '', }, { - text: 'Contact us', - url: '/help/contact-us', - dataLinkName: 'uk : footer : contact us', + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2025/09/05/Tax_strategy_for_the_year_ended_31_March_2025.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', extraClasses: '', }, ], @@ -2036,15 +1578,9 @@ export const Audio: FEArticle = { extraClasses: '', }, { - text: 'Modern Slavery Act', - url: 'https://uploads.guim.co.uk/2024/09/04/Modern_Slavery_Statement_2024_.pdf', - dataLinkName: 'uk : footer : modern slavery act statement', - extraClasses: '', - }, - { - text: 'Tax strategy', - url: 'https://uploads.guim.co.uk/2024/08/27/TAX_STRATEGY_FOR_THE_YEAR_ENDED_31_MARCH_2025.pdf', - dataLinkName: 'uk : footer : tax strategy', + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', extraClasses: '', }, { @@ -2054,33 +1590,45 @@ export const Audio: FEArticle = { extraClasses: '', }, { - text: 'Facebook', - url: 'https://www.facebook.com/theguardian', - dataLinkName: 'uk : footer : facebook', + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', extraClasses: '', }, { - text: 'YouTube', - url: 'https://www.youtube.com/user/TheGuardian', - dataLinkName: 'uk : footer : youtube', + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', extraClasses: '', }, { text: 'Instagram', url: 'https://www.instagram.com/guardian', - dataLinkName: 'uk : footer : instagram', + dataLinkName: 'uk : footer : Instagram', extraClasses: '', }, { text: 'LinkedIn', url: 'https://www.linkedin.com/company/theguardian', - dataLinkName: 'uk : footer : linkedin', + dataLinkName: 'uk : footer : LinkedIn', extraClasses: '', }, { - text: 'Newsletters', - url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', - dataLinkName: 'uk : footer : newsletters', + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', extraClasses: '', }, ], @@ -2110,9 +1658,9 @@ export const Audio: FEArticle = { extraClasses: '', }, { - text: 'Tips', - url: 'https://www.theguardian.com/tips', - dataLinkName: 'uk : footer : tips', + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', extraClasses: '', }, { @@ -2124,7 +1672,7 @@ export const Audio: FEArticle = { ], ], }, - publication: 'The Guardian', + publication: 'theguardian.com', shouldHideReaderRevenue: false, slotMachineFlags: '', contributionsServiceUrl: 'https://contributions.guardianapis.com', diff --git a/dotcom-rendering/scripts/test-data/gen-fixtures.js b/dotcom-rendering/scripts/test-data/gen-fixtures.js index 50cf15549c0..c0db29fd386 100644 --- a/dotcom-rendering/scripts/test-data/gen-fixtures.js +++ b/dotcom-rendering/scripts/test-data/gen-fixtures.js @@ -44,7 +44,7 @@ const articles = [ }, { name: 'Audio', - url: 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order', + url: 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast', }, { name: 'StandardWithVideo', diff --git a/dotcom-rendering/src/layouts/AudioLayout.stories.tsx b/dotcom-rendering/src/layouts/AudioLayout.stories.tsx new file mode 100644 index 00000000000..9227a2e43d9 --- /dev/null +++ b/dotcom-rendering/src/layouts/AudioLayout.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { allModes } from '../../.storybook/modes'; +import { Audio as AudioFixture } from '../../fixtures/generated/fe-articles/Audio'; +import { ArticleDesign } from '../lib/articleFormat'; +import { getCurrentPillar } from '../lib/layoutHelpers'; +import { extractNAV } from '../model/extract-nav'; +import { enhanceArticleType } from '../types/article'; +import { AudioLayout } from './AudioLayout'; + +const meta = { + title: 'Layouts/Audio', + component: AudioLayout, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const appsArticle = enhanceArticleType(AudioFixture, 'Apps'); + +if (appsArticle.design !== ArticleDesign.Audio) { + throw new Error( + `Expected ArticleDesign.Audio, got: ${String(appsArticle.design)}`, + ); +} + +export const Apps: Story = { + args: { + renderingTarget: 'Apps', + article: appsArticle.frontendData, + format: { + design: appsArticle.design, + display: appsArticle.display, + theme: appsArticle.theme, + }, + }, + parameters: { + formats: [ + { + design: appsArticle.design, + display: appsArticle.display, + theme: appsArticle.theme, + }, + ], + config: { + renderingTarget: 'Apps', + darkModeAvailable: true, + }, + chromatic: { + modes: { + 'light mobileMedium': allModes['light mobileMedium'], + }, + }, + }, +}; + +const webArticle = enhanceArticleType(AudioFixture, 'Web'); + +if (webArticle.design !== ArticleDesign.Audio) { + throw new Error( + `Expected ArticleDesign.Audio, got: ${String(webArticle.design)}`, + ); +} + +export const Web: Story = { + args: { + renderingTarget: 'Web', + NAV: { + ...extractNAV(webArticle.frontendData.nav), + selectedPillar: getCurrentPillar(webArticle.frontendData), + }, + article: webArticle.frontendData, + format: { + design: webArticle.design, + display: webArticle.display, + theme: webArticle.theme, + }, + }, + parameters: { + formats: [ + { + design: webArticle.design, + display: webArticle.display, + theme: webArticle.theme, + }, + ], + chromatic: { + modes: { + 'light leftCol': allModes['light leftCol'], + }, + }, + }, +}; diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index d4327da1612..73c2f5df4bf 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -6,11 +6,14 @@ import { until, } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { AdPortals } from '../components/AdPortals.importable'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; +import { AppsFooter } from '../components/AppsFooter.importable'; import { ArticleBody } from '../components/ArticleBody'; import { ArticleContainer } from '../components/ArticleContainer'; import { ArticleHeadline } from '../components/ArticleHeadline'; +import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { AudioPlayerWrapper } from '../components/AudioPlayerWrapper.importable'; @@ -134,8 +137,14 @@ interface WebProps extends Props { renderingTarget: 'Web'; } -export const AudioLayout = (props: WebProps) => { +interface AppProps extends Props { + renderingTarget: 'Apps'; +} + +export const AudioLayout = (props: WebProps | AppProps) => { const { article, format, renderingTarget, serverTime } = props; + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; const audioData = getAudioData(article.mainMediaElements); const { @@ -157,36 +166,38 @@ export const AudioLayout = (props: WebProps) => { return ( <> -
- {renderAds && ( - -
- -
-
- )} - -
+ {isWeb && ( +
+ {renderAds && ( + +
+ +
+
+ )} + +
+ )} - {format.theme === ArticleSpecial.Labs && ( + {isWeb && format.theme === ArticleSpecial.Labs && (
{ )} - {renderAds && hasSurveyAd && ( + {isWeb && renderAds && hasSurveyAd && ( )}
+ {isApps && renderAds && ( + + + + )}
{
- + {renderingTarget === 'Web' ? ( + + ) : ( + + )} {!!article.affiliateLinksDisclaimer && ( )}
- {audioData && ( + {isWeb && audioData && ( { shouldHideAds={article.shouldHideAds} idApiUrl={article.config.idApiUrl} /> - {showBodyEndSlot && ( + {isWeb && showBodyEndSlot && ( { -
- - - - - -
+ {isWeb && ( +
+ + + + + +
+ )}
- {renderAds && !isLabs && ( + {isWeb && renderAds && !isLabs && (
{ webURL={article.webURL} /> - {showComments && ( + {isWeb && showComments && (
{
)} - {renderAds && !isLabs && ( + {isWeb && renderAds && !isLabs && (
{ )}
- <> - {props.NAV.subNavSections && ( -
- + {props.NAV.subNavSections && ( +
- + + +
+ )} +
+
+
+ + + -
- )} -
-
+ -
- - - + + )} + {isApps && ( +
+ + - - - +
+ )} ); }; diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index b52abadf201..cfbc4ebc083 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -183,6 +183,15 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { renderingTarget={renderingTarget} /> ); + case ArticleDesign.Audio: + return ( + + ); default: return ( Date: Wed, 18 Mar 2026 11:52:50 +0000 Subject: [PATCH 02/17] Fix tsc --- .../fixtures/generated/fe-articles/Audio.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts index a8aa88c3734..76743078418 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts @@ -712,15 +712,10 @@ export const Audio: FEArticle = { { index: 1, fields: { - displayCredit: 'true', isMaster: 'true', - altText: 'Illustration: Anais Mims/Guardian Design', - mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', width: '3750', source: 'Getty / Guardian Design', - photographer: 'Anaïs Mims', height: '3000', - credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', }, mediaType: 'Image', url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/master/3750.jpg', @@ -728,14 +723,9 @@ export const Audio: FEArticle = { { index: 1, fields: { - displayCredit: 'true', - altText: 'Illustration: Anais Mims/Guardian Design', - mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', width: '3750', source: 'Getty / Guardian Design', - photographer: 'Anaïs Mims', height: '3000', - credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', }, mediaType: 'Image', url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/3750.jpg', @@ -743,14 +733,9 @@ export const Audio: FEArticle = { { index: 1, fields: { - displayCredit: 'true', - altText: 'Illustration: Anais Mims/Guardian Design', - mediaId: '5ef9f1435cd06589bf2a031119b64d38633d4197', width: '500', source: 'Getty / Guardian Design', - photographer: 'Anaïs Mims', height: '400', - credit: 'Illustration: Anaïs Mims/Getty / Guardian Design', }, mediaType: 'Image', url: 'https://media.guim.co.uk/5ef9f1435cd06589bf2a031119b64d38633d4197/625_0_3750_3000/500.jpg', From e2391400735aff1fe6d12c6e63f7ecae3a60c042 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:11:11 +0000 Subject: [PATCH 03/17] Add audio article button --- .../.storybook/mocks/bridgetApi.ts | 6 + dotcom-rendering/package.json | 2 +- .../src/components/AppsAudioPlayButton.tsx | 112 ++++++++++++++++++ .../components/AppsAudioPlayer.importable.tsx | 56 +++++++++ dotcom-rendering/src/layouts/AudioLayout.tsx | 19 +++ dotcom-rendering/src/lib/audio-data.ts | 9 +- dotcom-rendering/src/lib/bridgetApi.ts | 13 ++ pnpm-lock.yaml | 28 ++--- 8 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 dotcom-rendering/src/components/AppsAudioPlayButton.tsx create mode 100644 dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx diff --git a/dotcom-rendering/.storybook/mocks/bridgetApi.ts b/dotcom-rendering/.storybook/mocks/bridgetApi.ts index 7026003236a..d0b13dfdec9 100644 --- a/dotcom-rendering/.storybook/mocks/bridgetApi.ts +++ b/dotcom-rendering/.storybook/mocks/bridgetApi.ts @@ -103,6 +103,11 @@ export const getListenToArticleClient: BridgetApi< isPlaying: async () => false, }); +export const getAudioClient: BridgetApi<'getAudioClient'> = () => ({ + isAvailable: async () => true, + isPlaying: async () => false, +}); + export const getNativeABTestingClient: BridgetApi< 'getNativeABTestingClient' > = () => ({ @@ -127,6 +132,7 @@ export const ensure_all_exports_are_present = { getInteractionClient, getInteractivesClient, getListenToArticleClient, + getAudioClient, getNativeABTestingClient, } satisfies { [Method in keyof BridgeModule]: BridgetApi; diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 47e3eb17a92..b94534e02cf 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -29,7 +29,7 @@ "@guardian/ab-core": "8.0.0", "@guardian/ab-testing-config": "workspace:ab-testing-config", "@guardian/braze-components": "22.2.0", - "@guardian/bridget": "8.7.0", + "@guardian/bridget": "8.7.7-2026-03-05", "@guardian/browserslist-config": "6.1.0", "@guardian/cdk": "62.3.5", "@guardian/commercial-core": "29.0.0", diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx new file mode 100644 index 00000000000..dd87645f19e --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx @@ -0,0 +1,112 @@ +import { css } from '@emotion/react'; +import { from, height, space } from '@guardian/source/foundations'; +import type { ThemeIcon } from '@guardian/source/react-components'; +import { + Button, + SvgMediaControlsPlay, +} from '@guardian/source/react-components'; +import { palette } from '../palette'; +import type { WaveFormTheme } from './WaveForm'; +import { WaveForm } from './WaveForm'; + +const buttonCss = (audioDuration: string | undefined) => css` + display: flex; + align-items: center; + background-color: ${palette('--listen-to-article-button-fill')}; + color: ${palette('--listen-to-article-button-background')}; + &:active, + &:focus, + &:hover { + background-color: ${palette('--listen-to-article-button-fill')}; + } + margin-bottom: ${space[4]}px; + margin-left: ${space[2]}px; + padding-left: ${space[3]}px; + padding-right: ${audioDuration === undefined ? space[4] : space[3]}px; + padding-bottom: 0px; + font-size: 15px; + height: ${height.ctaXsmall}px; + min-height: ${height.ctaXsmall}px; + + .src-button-space { + width: 0px; + } +`; + +const themeIcon: ThemeIcon = { + fill: palette('--follow-icon-background'), +}; + +const waveFormContainerCss = css` + height: ${space[12]}px; + border-top: 1px solid ${palette('--article-meta-lines')}; + position: relative; + padding-top: ${space[2]}px; + overflow: hidden; + + > svg { + position: absolute; + top: ${space[2]}px; + left: 0; + width: 746px; + height: 100%; + z-index: 0; + } + + > button { + position: relative; + z-index: 1; + ${from.tablet} { + margin-left: 0; + } + } +`; + +const waveTheme: WaveFormTheme = { + wave: palette('--listen-to-article-waveform'), +}; + +const dividerCss = css` + width: 0.5px; + height: 100%; + opacity: 0.5; + border-left: 1px solid ${palette('--follow-icon-background')}; + margin-left: ${space[2]}px; +`; + +type Props = { + onClickHandler: () => void; + audioDuration?: string; +}; + +export const AppsAudioPlayButton = ({ + onClickHandler, + audioDuration, +}: Props) => { + return ( +
+ + +
+ ); +}; diff --git a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx new file mode 100644 index 00000000000..23392de26da --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx @@ -0,0 +1,56 @@ +import { log } from '@guardian/libs'; +import { useEffect, useState } from 'react'; +import { getAudioClient } from '../lib/bridgetApi'; +import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; +import { AppsAudioPlayButton } from './AppsAudioPlayButton'; + +const AUDIO_BRIDGET_VERSION = '8.7.7'; + +type Props = { + audioDuration?: string; +}; + +export const AppsAudioPlayer = ({ audioDuration }: Props) => { + const [showButton, setShowButton] = useState(false); + + const isBridgetCompatible = useIsBridgetCompatible(AUDIO_BRIDGET_VERSION); + + useEffect(() => { + if (isBridgetCompatible) { + Promise.all([ + getAudioClient().isAvailable(), + getAudioClient().isPlaying(), + ]) + .then(([isAvailable, isPlaying]) => { + setShowButton(isAvailable && !isPlaying); + }) + .catch((error: Error) => { + log('dotcom', 'Error fetching audio status: ', error); + setShowButton(false); + }); + } + }, [isBridgetCompatible]); + + const playHandler = () => { + void getAudioClient() + .play() + .then(() => { + // Hide the button once audio is playing + setShowButton(false); + }) + .catch((error: Error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getAudioClient-play-error', + ); + log('dotcom', 'Bridget getAudioClient.play Error:', error); + }); + }; + + return showButton ? ( + + ) : null; +}; diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index 73c2f5df4bf..f3774140c49 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -9,6 +9,7 @@ import { StraightLines } from '@guardian/source-development-kitchen/react-compon import { AdPortals } from '../components/AdPortals.importable'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; +import { AppsAudioPlayer } from '../components/AppsAudioPlayer.importable'; import { AppsFooter } from '../components/AppsFooter.importable'; import { ArticleBody } from '../components/ArticleBody'; import { ArticleContainer } from '../components/ArticleContainer'; @@ -25,6 +26,7 @@ import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; import { Island } from '../components/Island'; import { LabsHeader } from '../components/LabsHeader'; +import { formatAudioDuration } from '../components/ListenToArticle.importable'; import { Masthead } from '../components/Masthead/Masthead'; import { MostViewedFooterData } from '../components/MostViewedFooterData.importable'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; @@ -331,6 +333,23 @@ export const AudioLayout = (props: WebProps | AppProps) => { />
)} + {isApps && ( + + + + )} { +): + | { audioDownloadUrl: string; mediaId: string; durationSeconds?: number } + | undefined => { const audioBlockElement = mainMediaElements?.find( (element) => element._type === 'model.dotcomrendering.pageElements.AudioBlockElement', ); if (audioBlockElement?.assets[0] && audioBlockElement.id) { + const { fields } = audioBlockElement.assets[0]; + const mins = parseInt(fields?.durationMinutes ?? '0', 10); + const secs = parseInt(fields?.durationSeconds ?? '0', 10); + const total = (isNaN(mins) ? 0 : mins) * 60 + (isNaN(secs) ? 0 : secs); return { audioDownloadUrl: audioBlockElement.assets[0].url, mediaId: audioBlockElement.id, + durationSeconds: total > 0 ? total : undefined, }; } return undefined; diff --git a/dotcom-rendering/src/lib/bridgetApi.ts b/dotcom-rendering/src/lib/bridgetApi.ts index 644fb63e608..601df608ac7 100644 --- a/dotcom-rendering/src/lib/bridgetApi.ts +++ b/dotcom-rendering/src/lib/bridgetApi.ts @@ -1,6 +1,7 @@ import * as AbTesting from '@guardian/bridget/AbTesting'; import * as Acquisitions from '@guardian/bridget/Acquisitions'; import * as Analytics from '@guardian/bridget/Analytics'; +import * as Audio from '@guardian/bridget/Audio'; import * as Commercial from '@guardian/bridget/Commercial'; import * as Discussion from '@guardian/bridget/Discussion'; import * as Environment from '@guardian/bridget/Environment'; @@ -194,6 +195,18 @@ export const getInteractivesClient = (): Interactives.Client => { return interactivesClient; }; +let audioClient: Audio.Client | undefined = undefined; +export const getAudioClient = (): Audio.Client => { + if (!audioClient) { + audioClient = createAppClient>( + Audio.Client, + 'buffered', + 'compact', + ); + } + return audioClient; +}; + let listenToArticleClient: ListenToArticle.Client | undefined = undefined; export const getListenToArticleClient = (): ListenToArticle.Client => { if (!listenToArticleClient) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 768d85170e3..302ab2de3f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,8 +299,8 @@ importers: specifier: 22.2.0 version: 22.2.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@guardian/libs@30.1.1(@guardian/ophan-tracker-js@2.8.0)(tslib@2.6.2)(typescript@5.5.3))(@guardian/source@11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3))(react@18.3.1) '@guardian/bridget': - specifier: 8.7.0 - version: 8.7.0 + specifier: 8.7.7-2026-03-05 + version: 8.7.7-2026-03-05 '@guardian/browserslist-config': specifier: 6.1.0 version: 6.1.0(browserslist@4.24.4)(tslib@2.6.2) @@ -2535,8 +2535,8 @@ packages: '@guardian/source': ^9.0.0 react: 17.0.2 || 18.2.0 - '@guardian/bridget@8.7.0': - resolution: {integrity: sha512-tqOHEDGfMz+UnDndwrmmatuvElgguaxjYNopvDBZ7W6DCxswvEvhCZfZZ56Q2msF+jr042XNpVPcKMsrHF8VJw==} + '@guardian/bridget@8.7.7-2026-03-05': + resolution: {integrity: sha512-SB/axyvsduRiw7+iPmlrg9BJftUUQoV7QBx0A8eXwaveFb/kLn8uTLocoj4btjl8wir1CjpT3h4QKE+F3wAMHQ==} '@guardian/browserslist-config@6.1.0': resolution: {integrity: sha512-qM0QxAv6E5IHXny5Okli6AZXEio0mpXzzEzz38qrb4IwO91R6eWVKyihdj0qW2k7TVxMFVOSfNmBZ1H5EiJhgw==} @@ -6404,7 +6404,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -12445,7 +12445,7 @@ snapshots: '@guardian/source': 11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3) react: 18.3.1 - '@guardian/bridget@8.7.0': {} + '@guardian/bridget@8.7.7-2026-03-05': {} '@guardian/browserslist-config@6.1.0(browserslist@4.24.4)(tslib@2.6.2)': dependencies: @@ -12486,12 +12486,12 @@ snapshots: '@guardian/eslint-config-typescript@12.0.0(eslint@8.57.1)(tslib@2.6.2)(typescript@5.5.3)': dependencies: - '@guardian/eslint-config': 9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)(tslib@2.6.2) + '@guardian/eslint-config': 9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)(tslib@2.6.2) '@stylistic/eslint-plugin': 2.6.2(eslint@8.57.1)(typescript@5.5.3) '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1)(typescript@5.5.3) '@typescript-eslint/parser': 8.1.0(eslint@8.57.1)(typescript@5.5.3) eslint: 8.57.1 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) tslib: 2.6.2 typescript: 5.5.3 @@ -12521,7 +12521,7 @@ snapshots: - supports-color - typescript - '@guardian/eslint-config@9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)(tslib@2.6.2)': + '@guardian/eslint-config@9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)(tslib@2.6.2)': dependencies: eslint: 8.57.1 eslint-config-prettier: 9.1.0(eslint@8.57.1) @@ -16538,12 +16538,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: debug: 4.4.3(supports-color@8.1.1) enhanced-resolve: 5.19.0 eslint: 8.57.1 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) fast-glob: 3.3.3 get-tsconfig: 4.7.2 @@ -16572,14 +16572,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.1.0(eslint@8.57.1)(typescript@5.5.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -16634,7 +16634,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 77e94472ee18fd752a0ddb9961db58bb52563846 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:20:41 +0000 Subject: [PATCH 04/17] Listen to this podcast --- dotcom-rendering/src/components/AppsAudioPlayButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx index dd87645f19e..c5205ec76ba 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx @@ -98,7 +98,7 @@ export const AppsAudioPlayButton = ({ size="default" cssOverrides={buttonCss(audioDuration)} > - Listen to this episode + Listen to this podcast {audioDuration !== undefined && audioDuration !== '' && ( <> From 3c2f99d12fa17d7e973000815b83d37aa5717e9d Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:57:24 +0000 Subject: [PATCH 05/17] Create AppsAudioPlayButton.stories.tsx --- .../AppsAudioPlayButton.stories.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx new file mode 100644 index 00000000000..3234d7b953f --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { AppsAudioPlayButton as AppsAudioPlayButtonComponent } from './AppsAudioPlayButton'; + +const meta = { + component: AppsAudioPlayButtonComponent, + title: 'Components/Apps Audio Play Button', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithDuration: Story = { + args: { + onClickHandler: () => undefined, + audioDuration: '26:00', + }, +}; + +export const NoDuration: Story = { + args: { + onClickHandler: () => undefined, + audioDuration: undefined, + }, +}; From 9b4b47b273ffb5141d1378354f972441233cad2e Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:39:59 +0000 Subject: [PATCH 06/17] Add podcast meta --- .../src/components/ArticleMeta.apps.tsx | 43 ++++++++++++++++++- .../src/components/ArticleMeta.web.tsx | 6 +-- dotcom-rendering/src/layouts/AudioLayout.tsx | 3 ++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index fe34af72e1e..c404d4f4b02 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -10,11 +10,19 @@ import { ArticleDisplay, type ArticleFormat, } from '../lib/articleFormat'; +import { getAudioData } from '../lib/audio-data'; import { getSoleContributor } from '../lib/byline'; import { palette as themePalette } from '../palette'; import type { Branding as BrandingType } from '../types/branding'; +import type { FEElement } from '../types/content'; import type { TagType } from '../types/tag'; -import { shouldShowAvatar, shouldShowContributor } from './ArticleMeta.web'; +import { + getPodcast, + getRssFeedUrl, + getSeriesTag, + shouldShowAvatar, + shouldShowContributor, +} from './ArticleMeta.web'; import { Avatar } from './Avatar'; import { Branding } from './Branding.importable'; import { CommentCount } from './CommentCount.importable'; @@ -24,6 +32,7 @@ import { FollowWrapper } from './FollowWrapper.importable'; import { Island } from './Island'; import { ListenToArticle } from './ListenToArticle.importable'; import { LiveblogNotifications } from './LiveblogNotifications.importable'; +import { PodcastMeta } from './PodcastMeta'; type Props = { format: ArticleFormat; @@ -37,6 +46,7 @@ type Props = { isCommentable: boolean; pageId?: string; headline?: string; + mainMediaElements?: FEElement[]; }; const metaGridContainer = css` @@ -60,6 +70,17 @@ const metaGridContainer = css` } `; +const podcastMetaPadding = css` + ${until.phablet} { + padding-left: 20px; + padding-right: 20px; + } + ${until.mobileLandscape} { + padding-left: 10px; + padding-right: 10px; + } +`; + const metaContainerMargins = css` ${until.phablet} { margin-left: -20px; @@ -230,6 +251,7 @@ export const ArticleMetaApps = ({ isCommentable, pageId, headline, + mainMediaElements, }: Props) => { const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; @@ -245,6 +267,12 @@ export const ArticleMetaApps = ({ const isLiveBlog = format.design === ArticleDesign.LiveBlog; const isGallery = format.design === ArticleDesign.Gallery; const isVideo = format.design === ArticleDesign.Video; + const isAudio = format.design === ArticleDesign.Audio; + + const seriesTag = getSeriesTag(tags); + const podcast = getPodcast(tags); + const audioData = getAudioData(mainMediaElements); + const rssFeedUrl = getRssFeedUrl(tags); const shouldShowFollowButtons = (layoutOrDesignType: boolean) => layoutOrDesignType && !!byline && !isUndefined(soleContributor); @@ -268,6 +296,19 @@ export const ArticleMetaApps = ({ isGallery ? galleryMetaContainer : undefined, ]} > + {isAudio && podcast && seriesTag && ( +
+ +
+ )}
{ +export const getSeriesTag = (tags: TagType[]): TagType | undefined => { return tags.find((tag) => tag.type === 'Series' && tag.podcast); }; -const getPodcast = (tags: TagType[]): Podcast | undefined => { +export const getPodcast = (tags: TagType[]): Podcast | undefined => { const seriesTag = getSeriesTag(tags); return seriesTag?.podcast; }; -const getRssFeedUrl = (tags: TagType[]): string => { +export const getRssFeedUrl = (tags: TagType[]): string => { const seriesTag = getSeriesTag(tags); return `/${seriesTag?.id}/podcast.xml`; diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index 73c2f5df4bf..d064f98a667 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -306,6 +306,9 @@ export const AudioLayout = (props: WebProps | AppProps) => { article.config.discussionApiUrl } shortUrlId={article.config.shortUrlId} + mainMediaElements={ + article.mainMediaElements + } /> )} {!!article.affiliateLinksDisclaimer && ( From 0fb9b84bbafc9136a0fff7fcfc816a55dc4689a3 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:11:11 +0000 Subject: [PATCH 07/17] Add audio article button --- .../.storybook/mocks/bridgetApi.ts | 6 + dotcom-rendering/package.json | 2 +- .../src/components/AppsAudioPlayButton.tsx | 112 ++++++++++++++++++ .../components/AppsAudioPlayer.importable.tsx | 56 +++++++++ dotcom-rendering/src/layouts/AudioLayout.tsx | 19 +++ dotcom-rendering/src/lib/audio-data.ts | 9 +- dotcom-rendering/src/lib/bridgetApi.ts | 13 ++ pnpm-lock.yaml | 28 ++--- 8 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 dotcom-rendering/src/components/AppsAudioPlayButton.tsx create mode 100644 dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx diff --git a/dotcom-rendering/.storybook/mocks/bridgetApi.ts b/dotcom-rendering/.storybook/mocks/bridgetApi.ts index 7026003236a..d0b13dfdec9 100644 --- a/dotcom-rendering/.storybook/mocks/bridgetApi.ts +++ b/dotcom-rendering/.storybook/mocks/bridgetApi.ts @@ -103,6 +103,11 @@ export const getListenToArticleClient: BridgetApi< isPlaying: async () => false, }); +export const getAudioClient: BridgetApi<'getAudioClient'> = () => ({ + isAvailable: async () => true, + isPlaying: async () => false, +}); + export const getNativeABTestingClient: BridgetApi< 'getNativeABTestingClient' > = () => ({ @@ -127,6 +132,7 @@ export const ensure_all_exports_are_present = { getInteractionClient, getInteractivesClient, getListenToArticleClient, + getAudioClient, getNativeABTestingClient, } satisfies { [Method in keyof BridgeModule]: BridgetApi; diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 47e3eb17a92..b94534e02cf 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -29,7 +29,7 @@ "@guardian/ab-core": "8.0.0", "@guardian/ab-testing-config": "workspace:ab-testing-config", "@guardian/braze-components": "22.2.0", - "@guardian/bridget": "8.7.0", + "@guardian/bridget": "8.7.7-2026-03-05", "@guardian/browserslist-config": "6.1.0", "@guardian/cdk": "62.3.5", "@guardian/commercial-core": "29.0.0", diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx new file mode 100644 index 00000000000..dd87645f19e --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx @@ -0,0 +1,112 @@ +import { css } from '@emotion/react'; +import { from, height, space } from '@guardian/source/foundations'; +import type { ThemeIcon } from '@guardian/source/react-components'; +import { + Button, + SvgMediaControlsPlay, +} from '@guardian/source/react-components'; +import { palette } from '../palette'; +import type { WaveFormTheme } from './WaveForm'; +import { WaveForm } from './WaveForm'; + +const buttonCss = (audioDuration: string | undefined) => css` + display: flex; + align-items: center; + background-color: ${palette('--listen-to-article-button-fill')}; + color: ${palette('--listen-to-article-button-background')}; + &:active, + &:focus, + &:hover { + background-color: ${palette('--listen-to-article-button-fill')}; + } + margin-bottom: ${space[4]}px; + margin-left: ${space[2]}px; + padding-left: ${space[3]}px; + padding-right: ${audioDuration === undefined ? space[4] : space[3]}px; + padding-bottom: 0px; + font-size: 15px; + height: ${height.ctaXsmall}px; + min-height: ${height.ctaXsmall}px; + + .src-button-space { + width: 0px; + } +`; + +const themeIcon: ThemeIcon = { + fill: palette('--follow-icon-background'), +}; + +const waveFormContainerCss = css` + height: ${space[12]}px; + border-top: 1px solid ${palette('--article-meta-lines')}; + position: relative; + padding-top: ${space[2]}px; + overflow: hidden; + + > svg { + position: absolute; + top: ${space[2]}px; + left: 0; + width: 746px; + height: 100%; + z-index: 0; + } + + > button { + position: relative; + z-index: 1; + ${from.tablet} { + margin-left: 0; + } + } +`; + +const waveTheme: WaveFormTheme = { + wave: palette('--listen-to-article-waveform'), +}; + +const dividerCss = css` + width: 0.5px; + height: 100%; + opacity: 0.5; + border-left: 1px solid ${palette('--follow-icon-background')}; + margin-left: ${space[2]}px; +`; + +type Props = { + onClickHandler: () => void; + audioDuration?: string; +}; + +export const AppsAudioPlayButton = ({ + onClickHandler, + audioDuration, +}: Props) => { + return ( +
+ + +
+ ); +}; diff --git a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx new file mode 100644 index 00000000000..23392de26da --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx @@ -0,0 +1,56 @@ +import { log } from '@guardian/libs'; +import { useEffect, useState } from 'react'; +import { getAudioClient } from '../lib/bridgetApi'; +import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; +import { AppsAudioPlayButton } from './AppsAudioPlayButton'; + +const AUDIO_BRIDGET_VERSION = '8.7.7'; + +type Props = { + audioDuration?: string; +}; + +export const AppsAudioPlayer = ({ audioDuration }: Props) => { + const [showButton, setShowButton] = useState(false); + + const isBridgetCompatible = useIsBridgetCompatible(AUDIO_BRIDGET_VERSION); + + useEffect(() => { + if (isBridgetCompatible) { + Promise.all([ + getAudioClient().isAvailable(), + getAudioClient().isPlaying(), + ]) + .then(([isAvailable, isPlaying]) => { + setShowButton(isAvailable && !isPlaying); + }) + .catch((error: Error) => { + log('dotcom', 'Error fetching audio status: ', error); + setShowButton(false); + }); + } + }, [isBridgetCompatible]); + + const playHandler = () => { + void getAudioClient() + .play() + .then(() => { + // Hide the button once audio is playing + setShowButton(false); + }) + .catch((error: Error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getAudioClient-play-error', + ); + log('dotcom', 'Bridget getAudioClient.play Error:', error); + }); + }; + + return showButton ? ( + + ) : null; +}; diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index d064f98a667..a9c3de4d830 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -9,6 +9,7 @@ import { StraightLines } from '@guardian/source-development-kitchen/react-compon import { AdPortals } from '../components/AdPortals.importable'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; +import { AppsAudioPlayer } from '../components/AppsAudioPlayer.importable'; import { AppsFooter } from '../components/AppsFooter.importable'; import { ArticleBody } from '../components/ArticleBody'; import { ArticleContainer } from '../components/ArticleContainer'; @@ -25,6 +26,7 @@ import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; import { Island } from '../components/Island'; import { LabsHeader } from '../components/LabsHeader'; +import { formatAudioDuration } from '../components/ListenToArticle.importable'; import { Masthead } from '../components/Masthead/Masthead'; import { MostViewedFooterData } from '../components/MostViewedFooterData.importable'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; @@ -334,6 +336,23 @@ export const AudioLayout = (props: WebProps | AppProps) => { /> )} + {isApps && ( + + + + )} { +): + | { audioDownloadUrl: string; mediaId: string; durationSeconds?: number } + | undefined => { const audioBlockElement = mainMediaElements?.find( (element) => element._type === 'model.dotcomrendering.pageElements.AudioBlockElement', ); if (audioBlockElement?.assets[0] && audioBlockElement.id) { + const { fields } = audioBlockElement.assets[0]; + const mins = parseInt(fields?.durationMinutes ?? '0', 10); + const secs = parseInt(fields?.durationSeconds ?? '0', 10); + const total = (isNaN(mins) ? 0 : mins) * 60 + (isNaN(secs) ? 0 : secs); return { audioDownloadUrl: audioBlockElement.assets[0].url, mediaId: audioBlockElement.id, + durationSeconds: total > 0 ? total : undefined, }; } return undefined; diff --git a/dotcom-rendering/src/lib/bridgetApi.ts b/dotcom-rendering/src/lib/bridgetApi.ts index 644fb63e608..601df608ac7 100644 --- a/dotcom-rendering/src/lib/bridgetApi.ts +++ b/dotcom-rendering/src/lib/bridgetApi.ts @@ -1,6 +1,7 @@ import * as AbTesting from '@guardian/bridget/AbTesting'; import * as Acquisitions from '@guardian/bridget/Acquisitions'; import * as Analytics from '@guardian/bridget/Analytics'; +import * as Audio from '@guardian/bridget/Audio'; import * as Commercial from '@guardian/bridget/Commercial'; import * as Discussion from '@guardian/bridget/Discussion'; import * as Environment from '@guardian/bridget/Environment'; @@ -194,6 +195,18 @@ export const getInteractivesClient = (): Interactives.Client => { return interactivesClient; }; +let audioClient: Audio.Client | undefined = undefined; +export const getAudioClient = (): Audio.Client => { + if (!audioClient) { + audioClient = createAppClient>( + Audio.Client, + 'buffered', + 'compact', + ); + } + return audioClient; +}; + let listenToArticleClient: ListenToArticle.Client | undefined = undefined; export const getListenToArticleClient = (): ListenToArticle.Client => { if (!listenToArticleClient) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 768d85170e3..302ab2de3f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,8 +299,8 @@ importers: specifier: 22.2.0 version: 22.2.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@guardian/libs@30.1.1(@guardian/ophan-tracker-js@2.8.0)(tslib@2.6.2)(typescript@5.5.3))(@guardian/source@11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3))(react@18.3.1) '@guardian/bridget': - specifier: 8.7.0 - version: 8.7.0 + specifier: 8.7.7-2026-03-05 + version: 8.7.7-2026-03-05 '@guardian/browserslist-config': specifier: 6.1.0 version: 6.1.0(browserslist@4.24.4)(tslib@2.6.2) @@ -2535,8 +2535,8 @@ packages: '@guardian/source': ^9.0.0 react: 17.0.2 || 18.2.0 - '@guardian/bridget@8.7.0': - resolution: {integrity: sha512-tqOHEDGfMz+UnDndwrmmatuvElgguaxjYNopvDBZ7W6DCxswvEvhCZfZZ56Q2msF+jr042XNpVPcKMsrHF8VJw==} + '@guardian/bridget@8.7.7-2026-03-05': + resolution: {integrity: sha512-SB/axyvsduRiw7+iPmlrg9BJftUUQoV7QBx0A8eXwaveFb/kLn8uTLocoj4btjl8wir1CjpT3h4QKE+F3wAMHQ==} '@guardian/browserslist-config@6.1.0': resolution: {integrity: sha512-qM0QxAv6E5IHXny5Okli6AZXEio0mpXzzEzz38qrb4IwO91R6eWVKyihdj0qW2k7TVxMFVOSfNmBZ1H5EiJhgw==} @@ -6404,7 +6404,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -12445,7 +12445,7 @@ snapshots: '@guardian/source': 11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3) react: 18.3.1 - '@guardian/bridget@8.7.0': {} + '@guardian/bridget@8.7.7-2026-03-05': {} '@guardian/browserslist-config@6.1.0(browserslist@4.24.4)(tslib@2.6.2)': dependencies: @@ -12486,12 +12486,12 @@ snapshots: '@guardian/eslint-config-typescript@12.0.0(eslint@8.57.1)(tslib@2.6.2)(typescript@5.5.3)': dependencies: - '@guardian/eslint-config': 9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)(tslib@2.6.2) + '@guardian/eslint-config': 9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)(tslib@2.6.2) '@stylistic/eslint-plugin': 2.6.2(eslint@8.57.1)(typescript@5.5.3) '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1)(typescript@5.5.3) '@typescript-eslint/parser': 8.1.0(eslint@8.57.1)(typescript@5.5.3) eslint: 8.57.1 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) tslib: 2.6.2 typescript: 5.5.3 @@ -12521,7 +12521,7 @@ snapshots: - supports-color - typescript - '@guardian/eslint-config@9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)(tslib@2.6.2)': + '@guardian/eslint-config@9.0.0(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)(tslib@2.6.2)': dependencies: eslint: 8.57.1 eslint-config-prettier: 9.1.0(eslint@8.57.1) @@ -16538,12 +16538,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: debug: 4.4.3(supports-color@8.1.1) enhanced-resolve: 5.19.0 eslint: 8.57.1 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) fast-glob: 3.3.3 get-tsconfig: 4.7.2 @@ -16572,14 +16572,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.1.0(eslint@8.57.1)(typescript@5.5.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -16634,7 +16634,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.1.0(eslint@8.57.1)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From b8c1a2897ada29eb86839fe352cc10ec3c4eef24 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:20:41 +0000 Subject: [PATCH 08/17] Listen to this podcast --- dotcom-rendering/src/components/AppsAudioPlayButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx index dd87645f19e..c5205ec76ba 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx @@ -98,7 +98,7 @@ export const AppsAudioPlayButton = ({ size="default" cssOverrides={buttonCss(audioDuration)} > - Listen to this episode + Listen to this podcast {audioDuration !== undefined && audioDuration !== '' && ( <> From a079ee0ccd2d52e62c00bc4b89880acb0fcadbd0 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:57:24 +0000 Subject: [PATCH 09/17] Create AppsAudioPlayButton.stories.tsx --- .../AppsAudioPlayButton.stories.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx new file mode 100644 index 00000000000..3234d7b953f --- /dev/null +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { AppsAudioPlayButton as AppsAudioPlayButtonComponent } from './AppsAudioPlayButton'; + +const meta = { + component: AppsAudioPlayButtonComponent, + title: 'Components/Apps Audio Play Button', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithDuration: Story = { + args: { + onClickHandler: () => undefined, + audioDuration: '26:00', + }, +}; + +export const NoDuration: Story = { + args: { + onClickHandler: () => undefined, + audioDuration: undefined, + }, +}; From c4036bbc79c440e84e12dee7b6e7a598085ebfcf Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:43:24 +0000 Subject: [PATCH 10/17] Audio bridget version --- dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx index 23392de26da..3b652ab2195 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx @@ -4,7 +4,7 @@ import { getAudioClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { AppsAudioPlayButton } from './AppsAudioPlayButton'; -const AUDIO_BRIDGET_VERSION = '8.7.7'; +const AUDIO_BRIDGET_VERSION = '8.7.7-2026-03-05'; type Props = { audioDuration?: string; From 5ed4b81eacf2154833df033d49a6f919999040b5 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:35:54 +0000 Subject: [PATCH 11/17] Set relative base url to fix cors issue --- dotcom-rendering/src/lib/assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/assets.ts b/dotcom-rendering/src/lib/assets.ts index 3ea97af35b1..40fade109b2 100644 --- a/dotcom-rendering/src/lib/assets.ts +++ b/dotcom-rendering/src/lib/assets.ts @@ -9,7 +9,7 @@ interface AssetHash { [key: string]: string; } -export const BASE_URL_DEV = 'http://localhost:3030/'; +export const BASE_URL_DEV = '/'; export type AssetOrigin = | 'https://assets.guim.co.uk/' From 00bc3af734185d76581e2e4dca439f9465254d28 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:22:39 +0000 Subject: [PATCH 12/17] Add polling for button --- .../components/AppsAudioPlayer.importable.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx index 3b652ab2195..d947adb48af 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx @@ -1,10 +1,11 @@ import { log } from '@guardian/libs'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { getAudioClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { AppsAudioPlayButton } from './AppsAudioPlayButton'; const AUDIO_BRIDGET_VERSION = '8.7.7-2026-03-05'; +const POLLING_INTERVAL_MS = 3000; type Props = { audioDuration?: string; @@ -12,9 +13,21 @@ type Props = { export const AppsAudioPlayer = ({ audioDuration }: Props) => { const [showButton, setShowButton] = useState(false); + const pollingRef = useRef | null>(null); const isBridgetCompatible = useIsBridgetCompatible(AUDIO_BRIDGET_VERSION); + const checkIsPlaying = useCallback(() => { + getAudioClient() + .isPlaying() + .then((isPlaying) => { + setShowButton(!isPlaying); + }) + .catch((error: Error) => { + log('dotcom', 'Error polling isPlaying: ', error); + }); + }, []); + useEffect(() => { if (isBridgetCompatible) { Promise.all([ @@ -23,13 +36,26 @@ export const AppsAudioPlayer = ({ audioDuration }: Props) => { ]) .then(([isAvailable, isPlaying]) => { setShowButton(isAvailable && !isPlaying); + + if (isAvailable) { + pollingRef.current = setInterval( + checkIsPlaying, + POLLING_INTERVAL_MS, + ); + } }) .catch((error: Error) => { log('dotcom', 'Error fetching audio status: ', error); setShowButton(false); }); } - }, [isBridgetCompatible]); + + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + }, [isBridgetCompatible, checkIsPlaying]); const playHandler = () => { void getAudioClient() From 41ada05895ad965c890f37ce0cb143b02305426a Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:37:18 +0000 Subject: [PATCH 13/17] Update assets.ts --- dotcom-rendering/src/lib/assets.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotcom-rendering/src/lib/assets.ts b/dotcom-rendering/src/lib/assets.ts index 40fade109b2..4c282617071 100644 --- a/dotcom-rendering/src/lib/assets.ts +++ b/dotcom-rendering/src/lib/assets.ts @@ -14,8 +14,7 @@ export const BASE_URL_DEV = '/'; export type AssetOrigin = | 'https://assets.guim.co.uk/' | 'https://assets-code.guim.co.uk/' - | typeof BASE_URL_DEV - | '/'; + | typeof BASE_URL_DEV; /** * Decides the url to use for fetching assets From d2ad777759b6d67c959667862368d20aea036cd6 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:44:32 +0000 Subject: [PATCH 14/17] Revert dev base url change --- dotcom-rendering/src/lib/assets.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/lib/assets.ts b/dotcom-rendering/src/lib/assets.ts index 4c282617071..3ea97af35b1 100644 --- a/dotcom-rendering/src/lib/assets.ts +++ b/dotcom-rendering/src/lib/assets.ts @@ -9,12 +9,13 @@ interface AssetHash { [key: string]: string; } -export const BASE_URL_DEV = '/'; +export const BASE_URL_DEV = 'http://localhost:3030/'; export type AssetOrigin = | 'https://assets.guim.co.uk/' | 'https://assets-code.guim.co.uk/' - | typeof BASE_URL_DEV; + | typeof BASE_URL_DEV + | '/'; /** * Decides the url to use for fetching assets From f46bba1bcb0adba23a0243a6f534c705c16a8d4c Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:51:51 +0000 Subject: [PATCH 15/17] bump bridget --- dotcom-rendering/package.json | 2 +- .../src/components/AppsAudioPlayer.importable.tsx | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 9a5475280f2..472183454a9 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -29,7 +29,7 @@ "@guardian/ab-core": "8.0.0", "@guardian/ab-testing-config": "workspace:ab-testing-config", "@guardian/braze-components": "22.2.0", - "@guardian/bridget": "8.7.7-2026-03-05", + "@guardian/bridget": "8.9.0", "@guardian/browserslist-config": "6.1.0", "@guardian/cdk": "62.6.1", "@guardian/commercial-core": "29.0.0", diff --git a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx index d947adb48af..85d8b2a95a2 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx @@ -4,7 +4,7 @@ import { getAudioClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { AppsAudioPlayButton } from './AppsAudioPlayButton'; -const AUDIO_BRIDGET_VERSION = '8.7.7-2026-03-05'; +const AUDIO_BRIDGET_VERSION = '8.9.0'; const POLLING_INTERVAL_MS = 3000; type Props = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f87444933c..f2891e4be7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,8 +294,8 @@ importers: specifier: 22.2.0 version: 22.2.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@guardian/libs@30.1.1(@guardian/ophan-tracker-js@2.8.0)(tslib@2.6.2)(typescript@5.5.3))(@guardian/source@11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3))(react@18.3.1) '@guardian/bridget': - specifier: 8.7.7-2026-03-05 - version: 8.7.7-2026-03-05 + specifier: 8.9.0 + version: 8.9.0 '@guardian/browserslist-config': specifier: 6.1.0 version: 6.1.0(browserslist@4.24.4)(tslib@2.6.2) @@ -2399,8 +2399,8 @@ packages: '@guardian/source': ^9.0.0 react: 17.0.2 || 18.2.0 - '@guardian/bridget@8.7.7-2026-03-05': - resolution: {integrity: sha512-SB/axyvsduRiw7+iPmlrg9BJftUUQoV7QBx0A8eXwaveFb/kLn8uTLocoj4btjl8wir1CjpT3h4QKE+F3wAMHQ==} + '@guardian/bridget@8.9.0': + resolution: {integrity: sha512-OAo45mzxGJeCVhfsle9r0J0tqOWcDL6EyL63324ovAwREC2Joj7zOnuhPPtFLx7pjvUl6qMQzuyIiWppZkFbiA==} '@guardian/browserslist-config@6.1.0': resolution: {integrity: sha512-qM0QxAv6E5IHXny5Okli6AZXEio0mpXzzEzz38qrb4IwO91R6eWVKyihdj0qW2k7TVxMFVOSfNmBZ1H5EiJhgw==} @@ -11547,7 +11547,7 @@ snapshots: '@guardian/source': 11.3.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3) react: 18.3.1 - '@guardian/bridget@8.7.7-2026-03-05': {} + '@guardian/bridget@8.9.0': {} '@guardian/browserslist-config@6.1.0(browserslist@4.24.4)(tslib@2.6.2)': dependencies: From b6e9d85979d65869134b36263c44be92c642f1d4 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:07:45 +0000 Subject: [PATCH 16/17] resolve merge conflicts --- dotcom-rendering/src/layouts/AudioLayout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index 0a195ad3b50..5abcc226cef 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -6,11 +6,11 @@ import { until, } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; -import { AdPortals } from '../components/AdPortals.importable'; +import { AdPortals } from '../components/AdPortals.island'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; import { AppsAudioPlayer } from '../components/AppsAudioPlayer.importable'; -import { AppsFooter } from '../components/AppsFooter.importable'; +import { AppsFooter } from '../components/AppsFooter.island'; import { ArticleBody } from '../components/ArticleBody'; import { ArticleContainer } from '../components/ArticleContainer'; import { ArticleHeadline } from '../components/ArticleHeadline'; @@ -26,7 +26,7 @@ import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; import { Island } from '../components/Island'; import { LabsHeader } from '../components/LabsHeader'; -import { formatAudioDuration } from '../components/ListenToArticle.importable'; +import { formatAudioDuration } from '../components/ListenToArticle.island'; import { Masthead } from '../components/Masthead/Masthead'; import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; From 6448e134a68aff17a72181f004e80560deab7a59 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:31:55 +0000 Subject: [PATCH 17/17] import shared styles --- .../src/components/AppsAudioPlayButton.tsx | 28 ++----------------- .../src/components/ListenToArticleButton.tsx | 2 +- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx index c5205ec76ba..c37a58cbcad 100644 --- a/dotcom-rendering/src/components/AppsAudioPlayButton.tsx +++ b/dotcom-rendering/src/components/AppsAudioPlayButton.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/react'; -import { from, height, space } from '@guardian/source/foundations'; +import { height, space } from '@guardian/source/foundations'; import type { ThemeIcon } from '@guardian/source/react-components'; import { Button, SvgMediaControlsPlay, } from '@guardian/source/react-components'; import { palette } from '../palette'; +import { waveFormContainerCss } from './ListenToArticleButton'; import type { WaveFormTheme } from './WaveForm'; import { WaveForm } from './WaveForm'; @@ -37,31 +38,6 @@ const themeIcon: ThemeIcon = { fill: palette('--follow-icon-background'), }; -const waveFormContainerCss = css` - height: ${space[12]}px; - border-top: 1px solid ${palette('--article-meta-lines')}; - position: relative; - padding-top: ${space[2]}px; - overflow: hidden; - - > svg { - position: absolute; - top: ${space[2]}px; - left: 0; - width: 746px; - height: 100%; - z-index: 0; - } - - > button { - position: relative; - z-index: 1; - ${from.tablet} { - margin-left: 0; - } - } -`; - const waveTheme: WaveFormTheme = { wave: palette('--listen-to-article-waveform'), }; diff --git a/dotcom-rendering/src/components/ListenToArticleButton.tsx b/dotcom-rendering/src/components/ListenToArticleButton.tsx index 2e09e8c5e5b..691ed099e52 100644 --- a/dotcom-rendering/src/components/ListenToArticleButton.tsx +++ b/dotcom-rendering/src/components/ListenToArticleButton.tsx @@ -45,7 +45,7 @@ const themeIcon: ThemeIcon = { fill: palette('--follow-icon-background'), }; -const waveFormContainerCss = css` +export const waveFormContainerCss = css` height: ${space[12]}px; border-top: 1px solid ${palette('--article-meta-lines')}; position: relative;