Merge pull request #235 from reuters-graphics/mf-before-after

Updates BeforeAfter
This commit is contained in:
MinamiFunakoshiTR 2025-04-07 10:19:15 -05:00 committed by GitHub
commit 8ad8024987
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 319 additions and 308 deletions

View file

@ -0,0 +1,111 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as BeforeAfterStories from './BeforeAfter.stories.svelte';
<Meta of={BeforeAfterStories} />
# BeforeAfter
The `BeforeAfter` component shows a before-and-after comparison of an image.
```svelte
<script>
import { BeforeAfter } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // If using in the Graphics Kit
</script>
<BeforeAfter
beforeSrc={`${assets}/images/before-after/myrne-before.jpg`}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc={`${assets}/images/before-after/myrne-after.jpg`}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
/>
```
<Canvas of={BeforeAfterStories.Demo} />
## Using with ArchieML docs
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
```yaml
# ArchieML doc
[blocks]
type: before-after
beforeSrc: images/before-after/myrne-before.jpg
beforeAlt: Satellite image of Russian base at Myrne taken on July 7, 2020.
afterSrc: images/before-after/myrne-after.jpg
afterAlt: Satellite image of Russian base at Myrne taken on Oct. 20, 2020.
[]
```
... which you'll parse out of a ArchieML block object before passing to the `BeforeAfter` component.
```svelte
<!-- App.svelte -->
{#each content.blocks as block}
{#if block.type === 'before-after'}
<BeforeAfter
beforeSrc={`${assets}/${block.beforeSrc}`}
beforeAlt={block.beforeAlt}
afterSrc={`${assets}/${block.afterSrc}`}
afterAlt={block.afterAlt}
/>
{/if}
{/each}
```
<Canvas of={BeforeAfterStories.Demo} />
## Adding text
To add text overlays and captions, use [snippets](https://svelte.dev/docs/svelte/snippet) for `beforeOverlay`, `afterOverlay` and `caption`. You can style the snippets to match your page design, like in [this demo](./?path=/story/components-multimedia-beforeafter--with-overlays).
> 💡**NOTE:** The text in the overlays are used as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your before and after images. You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but these ARIA descriptions provide additional information or context that the reader might need.
```svelte
<BeforeAfter
beforeSrc={beforeImg}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc={afterImg}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<!-- Optional custom text overlay for the before image -->
{#snippet beforeOverlay()}
<div class="overlay p-3 before text-left">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
{/snippet}
<!-- Optional custom text overlay for the after image -->
{#snippet afterOverlay()}
<div class="overlay p-3 after text-right">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
{/snippet}
<!-- Optional custom caption for both images -->
{#snippet caption()}
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
{/snippet}
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```
<Canvas of={BeforeAfterStories.WithText} />

View file

@ -1,19 +1,10 @@
<!-- @migration-task Error while migrating Svelte code: end is out of bounds -->
<script context="module" lang="ts">
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import BeforeAfter from './BeforeAfter.svelte';
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import withOverlaysDocs from './stories/docs/withOverlays.md?raw';
// @ts-ignore raw
import ariaDescriptionsDocs from './stories/docs/ariaDescriptions.md?raw';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
export const meta = {
title: 'Components/Graphics/BeforeAfter',
const { Story } = defineMeta({
title: 'Components/Multimedia/BeforeAfter',
component: BeforeAfter,
...withComponentDocs(componentDocs),
argTypes: {
handleColour: { control: 'color' },
width: {
@ -21,96 +12,54 @@
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
// @ts-ignore raw
import beforeImg from './stories/myrne-before.jpg';
// @ts-ignore raw
import afterImg from './stories/myrne-after.jpg';
import beforeImg from './images/myrne-before.jpg';
import afterImg from './images/myrne-after.jpg';
</script>
<Template let:args>
<BeforeAfter {...args} />
</Template>
<Story
name="Default"
args="{{
name="Demo"
args={{
beforeSrc: beforeImg,
beforeAlt:
'Satellite image of Russian base at Myrne taken on July 7, 2020.',
afterSrc: afterImg,
afterAlt:
'Satellite image of Russian base at Myrne taken on Oct. 20, 2020.',
}}"
}}
/>
<Story name="With overlays" {...withStoryDocs(withOverlaysDocs)}>
<Story name="With text" exportName="WithText">
<BeforeAfter
beforeSrc="{beforeImg}"
beforeSrc={beforeImg}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterSrc={afterImg}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
{#snippet beforeOverlay()}
<div class="overlay p-3 before text-left">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
{/snippet}
{#snippet afterOverlay()}
<div class="overlay p-3 after text-right">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
{/snippet}
{#snippet caption()}
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
{/snippet}
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
</Story>
<Story name="ARIA descriptions" {...withStoryDocs(ariaDescriptionsDocs)}>
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div let:description="{id}" slot="beforeOverlay" class="overlay p-3">
<p class="body-caption" {id}>
On July 7, 2020, the base contained only a few transport vehicles.
</p>
</div>
<div let:description="{id}" slot="afterOverlay" class="overlay p-3">
<!-- 👇 id can also be used on an element containing multiple text elements -->
<div {id}>
<p class="body-caption">
But by October, tanks and artillery could be seen.
</p>
<p class="body-caption">
In total, over 50 pieces of heavy machinery and 200 personnel are now
based here.
</p>
</div>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
div.overlay {
max-width: 250px;
background: rgba(0, 0, 0, 0.45);
p {
color: #ffffff;
}

View file

@ -1,119 +1,149 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `BeforeAfter` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-beforeafter--docs) -->
<script lang="ts">
import { type Snippet } from 'svelte';
import { throttle } from 'lodash-es';
import { onMount } from 'svelte';
import Block from '../Block/Block.svelte';
import type { ContainerWidth } from '../@types/global';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import type { ContainerWidth } from '../@types/global';
import { random4 } from '../../utils/';
/** Width of the chart within the text well. */
export let width: ContainerWidth = 'normal'; // options: wide, wider, widest, fluid
/** Height of the component */
export let height = 600;
interface Props {
/** Width of the chart within the text well. Options: wide, wider, widest, fluid */
width?: ContainerWidth;
/** Height of the component */
height?: number;
/**
* If set, makes the height a ratio of the component's width.
*/
heightRatio?: number;
/**
* Before image source
*/
beforeSrc: string;
/**
* Before image altText
*/
beforeAlt: string;
/**
* After image source
*/
afterSrc: string;
/**
* After image altText
*/
afterAlt: string;
/**
* Class to target with SCSS.
*/
class?: string;
/** Drag handle colour */
handleColour?: string;
/** Drag handle opacity */
handleInactiveOpacity?: number;
/** Margin at the edge of the image to stop dragging */
handleMargin?: number;
/** Percentage of the component width the handle will travel ona key press */
keyPressStep?: number;
/** Initial offset of the handle, between 0 and 1. */
offset?: number;
/** ID to target with SCSS. */
id?: string;
/**
* Optional snippet for a custom overlay for the before image.
*/
beforeOverlay?: Snippet;
/**
* Optional snippet for a custom overlay for the after image.
*/
afterOverlay?: Snippet;
/**
* Optional snippet for a custom caption.
*/
caption?: Snippet;
/** Custom ARIA label language to label the component. */
ariaLabel?: string;
}
/**
* If set, makes the height a ratio of the component's width.
* @type {number}
let {
width = 'normal',
height = 600,
heightRatio,
beforeSrc,
beforeAlt,
afterSrc,
afterAlt,
class: cls = '',
handleColour = 'white',
handleInactiveOpacity = 0.9,
handleMargin = 20,
keyPressStep = 0.05,
offset = 0.5,
id = 'before-after-' + random4() + random4(),
beforeOverlay,
afterOverlay,
caption,
ariaLabel = 'Stacked before and after images with an adjustable slider',
}: Props = $props();
/** DOM nodes are undefined until the component is mounted — in other words, you should read it inside an effect or an event handler, but not during component initialisation.
*/
export let heightRatio: number | null = null;
let img: HTMLImageElement | undefined = $state(undefined);
/**
* Before image src
* @required
*/
export let beforeSrc: string | null = null;
/**
* Before image altText
* @required
*/
export let beforeAlt: string | null = null;
/**
* After image src
* @required
*/
export let afterSrc: string | null = null;
/**
* After image altText
* @required
*/
export let afterAlt: string | null = null;
/**
* Set a class to target with SCSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
/** Drag handle colour */
export let handleColour = 'white';
/** Drag handle opacity */
export let handleInactiveOpacity = 0.9;
/** Margin at the edge of the image to stop dragging */
export let handleMargin = 20;
/** Percentage of the component width the handle will travel ona key press */
export let keyPressStep = 0.05;
/** Initial offset of the handle, between 0 and 1. */
export let offset = 0.5;
const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
/**
* Add an ID to target with SCSS.
* @type {string}
*/
export let id: string = 'before-after-' + random4() + random4();
let img: HTMLImageElement;
let imgOffset: DOMRect | null = null;
/** Defaults with an empty DOMRect with all values set to 0 */
let imgOffset: DOMRect = $state(new DOMRect());
let sliding = false;
let figure: HTMLElement;
let beforeOverlayWidth = 0;
let figure: HTMLElement | undefined = $state(undefined);
let beforeOverlayWidth = $state(0);
let isFocused = false;
let containerWidth: number;
let containerWidth: number = $state(0); // Defaults to 0
$: containerHeight =
containerWidth && heightRatio ? containerWidth * heightRatio : height;
let containerHeight = $derived(
containerWidth && heightRatio ? containerWidth * heightRatio : height
);
$: w = (imgOffset && imgOffset.width) || 0;
$: x = w * offset;
$: figStyle = `width:100%;height:${containerHeight}px;`;
$: imgStyle = 'width:100%;height:100%;';
$: beforeOverlayClip =
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0;
let w = $derived(imgOffset.width);
let x = $derived(w * offset);
let figStyle = $derived(`width:100%;height:${containerHeight}px;`);
const imgStyle = 'width:100%;height:100%;';
let beforeOverlayClip = $derived(
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0
);
const onFocus = () => (isFocused = true);
const onBlur = () => (isFocused = false);
/** Toggle `isFocused` */
const onfocus = () => (isFocused = true);
const onblur = () => (isFocused = false);
/** Handle left or right arrows being pressed */
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFocused) return;
const { keyCode } = e;
const { code, key } = e;
const margin = handleMargin / w;
if (keyCode === 39) {
if (code === 'ArrowRight' || key === 'ArrowRight') {
offset = Math.min(1 - margin, offset + keyPressStep);
} else if (keyCode === 37) {
} else if (code === 'ArrowLeft' || key === 'ArrowLeft') {
offset = Math.max(0 + margin, offset - keyPressStep);
}
};
/** Measure image and set image offset */
const measureImage = () => {
if (img && img.complete) imgOffset = img.getBoundingClientRect();
};
/** Reset image offset on resize */
const resize = () => {
measureImage();
};
/** Measure image and set image offset on load */
const measureLoadedImage = (e: Event) => {
if (e.type === 'load') {
imgOffset = (e.target as HTMLImageElement).getBoundingClientRect();
}
};
/** Move the slider */
const move = (e: MouseEvent | TouchEvent) => {
if (sliding && imgOffset) {
const el =
@ -130,115 +160,115 @@
offset = x / w;
}
};
/** Starts the slider */
const start = (e: MouseEvent | TouchEvent) => {
sliding = true;
move(e);
};
/** Sets `sliding` to `false`*/
const end = () => {
sliding = false;
};
/** Keep this warning since these values are often read from an ArchieML doc, which will not trigger typescript errors even if required values don't exist */
if (!(beforeSrc && beforeAlt && afterSrc && afterAlt)) {
console.warn('Missing required src or alt props for BeforeAfter component');
}
onMount(() => {
// This is necessary b/c on:load doesn't reliably fire on the image...
const interval = setInterval(() => {
if (imgOffset) clearInterval(interval);
if (img && img.complete && !imgOffset) measureImage();
}, 50);
});
/** @TODO - Double check if this onMount is still necessary */
// onMount(() => {
// // This is necessary b/c on:load doesn't reliably fire on the image...
// const interval = setInterval(() => {
// if (imgOffset) clearInterval(interval);
// if (img && img.complete && !imgOffset) measureImage();
// }, 50);
// });
</script>
<svelte:window
on:touchmove="{move}"
on:touchend="{end}"
on:mousemove="{move}"
on:mouseup="{end}"
on:resize="{throttle(resize, 100)}"
on:keydown="{handleKeyDown}"
ontouchmove={move}
ontouchend={end}
onmousemove={move}
onmouseup={end}
onresize={throttle(resize, 100)}
onkeydown={handleKeyDown}
/>
<!-- Since we usually read these values from ArchieML, check that they exist -->
{#if beforeSrc && beforeAlt && afterSrc && afterAlt}
<Block {width} {id} class="photo before-after fmy-6 {cls}">
<div
style="height: {containerHeight}px;"
bind:clientWidth="{containerWidth}"
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<figure
style="{figStyle}"
<div style="height: {containerHeight}px;" bind:clientWidth={containerWidth}>
<button
style={figStyle}
class="before-after-container relative overflow-hidden my-0 mx-auto"
on:touchstart="{start}"
on:mousedown="{start}"
bind:this="{figure}"
aria-labelledby="{($$slots.caption && `${id}-caption`) || undefined}"
ontouchstart={start}
onmousedown={start}
bind:this={figure}
aria-label={ariaLabel}
>
<img
bind:this="{img}"
src="{afterSrc}"
alt="{afterAlt}"
on:load="{measureLoadedImage}"
on:mousedown|preventDefault
style="{imgStyle}"
bind:this={img}
src={afterSrc}
alt={afterAlt}
onload={measureLoadedImage}
style={imgStyle}
class="after absolute block m-0 max-w-full object-cover"
aria-describedby="{($$slots.beforeOverlay && `${id}-before`) ||
undefined}"
aria-describedby={beforeOverlay ?
`${id}-before-description`
: undefined}
/>
<img
src="{beforeSrc}"
alt="{beforeAlt}"
on:mousedown|preventDefault
src={beforeSrc}
alt={beforeAlt}
style="clip: rect(0 {x}px {containerHeight}px 0);{imgStyle}"
class="before absolute block m-0 max-w-full object-cover"
aria-describedby="{($$slots.afterOverlay && `${id}-after`) ||
undefined}"
aria-describedby={afterOverlay ?
`${id}-after-description`
: undefined}
/>
{#if $$slots.beforeOverlay}
{#if beforeOverlay}
<div
id="image-before-label"
id="{id}-before-description"
class="overlay-container before absolute"
bind:clientWidth="{beforeOverlayWidth}"
bind:clientWidth={beforeOverlayWidth}
style="clip-path: inset(0 {beforeOverlayClip}px 0 0);"
>
<!-- Overlay for before image -->
<slot
name="beforeOverlay"
description="{`${id}-before-description`}"
/>
{@render beforeOverlay()}
</div>
{/if}
{#if $$slots.afterOverlay}
<div id="image-after-label" class="overlay-container after absolute">
{#if afterOverlay}
<div
id="{id}-after-description"
class="overlay-container after absolute"
>
<!-- Overlay for after image -->
<slot
name="afterOverlay"
description="{`${id}-after-description`}"
/>
{@render afterOverlay()}
</div>
{/if}
<div
tabindex="0"
role="slider"
aria-valuenow="{Math.round(offset * 100)}"
aria-valuenow={Math.round(offset * 100)}
class="handle"
style="left: calc({offset *
100}% - 20px); --before-after-handle-colour: {handleColour}; --before-after-handle-inactive-opacity: {handleInactiveOpacity};"
on:focus="{onFocus}"
on:blur="{onBlur}"
{onfocus}
{onblur}
>
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
</figure>
</button>
</div>
{#if $$slots.caption}
<PaddingReset containerIsFluid="{width === 'fluid'}">
<aside class="before-after-caption mx-auto" id="{`${id}-caption`}">
{#if caption}
<PaddingReset containerIsFluid={width === 'fluid'}>
<aside class="before-after-caption mx-auto" id={`${id}-caption`}>
<!-- Caption for image credits -->
<slot name="caption" />
{@render caption()}
</aside>
</PaddingReset>
{/if}
@ -246,9 +276,9 @@
{/if}
<style lang="scss">
@use '../../scss/mixins' as *;
@use '../../scss/mixins' as mixins;
figure.before-after-container {
button.before-after-container {
box-sizing: content-box;
img {
@ -264,7 +294,7 @@
user-select: none;
}
.overlay-container {
position: absolute;
top: 0;
:global(:first-child) {
margin-top: 0;
}
@ -337,9 +367,9 @@
}
}
aside.before-after-caption {
.before-after-caption {
:global(p) {
@include body-caption;
@include mixins.body-caption;
}
}
</style>

View file

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 715 KiB

View file

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 472 KiB

View file

@ -1,35 +0,0 @@
Use text elements in your overlays as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your images by setting an ID on text elements within each overlay with the `description` [slot prop](https://svelte.dev/tutorial/slot-props).
> **💡 TIP:** You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but you can use these descriptions to provide more information or context that the reader might also need.
```svelte
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```

View file

@ -1,15 +0,0 @@
A before and after image comparison component.
```svelte
<script>
import { BeforeAfter } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // If using in the Graphics Kit
</script>
<BeforeAfter
beforeSrc="{`${assets}/images/before-after/myrne-before.jpg`}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{`${assets}/images/before-after/myrne-after.jpg`}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
/>
```

View file

@ -1,33 +0,0 @@
Add overlays with the `beforeOverlay` and `afterOverlay` slots and a caption to the `caption` slot, then style these elements to match your page design as below.
```svelte
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```

View file

@ -17,12 +17,10 @@
publishTime: string;
/**
* Update time as a datetime string.
* @type {string}
*/
updateTime: string;
/**
* Alignment of the byline.
* @type {string}
*/
align?: 'left' | 'center';
/**

View file

@ -1,9 +1,15 @@
import slugify from 'slugify';
/** Helper function to generate a random 4-character string */
export const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
/**
* Custom function that returns an author page URL.
*/
export const getAuthorPageUrl = (author: string): string => {
const authorSlug = slugify(author.trim(), { lower: true });
return `https://www.reuters.com/authors/${authorSlug}/`;
};
};