diff --git a/dotcom-rendering/.storybook/preview.ts b/dotcom-rendering/.storybook/preview.ts
index 678a597e431..ed1b9394e85 100644
--- a/dotcom-rendering/.storybook/preview.ts
+++ b/dotcom-rendering/.storybook/preview.ts
@@ -24,6 +24,9 @@ import { palette as sourcePalette } from '@guardian/source/foundations';
sb.mock(import('../src/lib/useNewsletterSubscription.ts'), { spy: true });
sb.mock(import('../src/lib/useAuthStatus.ts'), { spy: true });
sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });
+sb.mock(import('../src/lib/useIsMyGuardianEnabled.ts'), { spy: true });
+sb.mock(import('../src/lib/useIsBridgetCompatible.ts'), { spy: true });
+sb.mock(import('../src/lib/bridgetApi.ts'), { spy: true });
// Prevent components being lazy rendered when we're taking Chromatic snapshots
Lazy.disabled = isChromatic();
diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx
new file mode 100644
index 00000000000..f2399bb0c4e
--- /dev/null
+++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx
@@ -0,0 +1,303 @@
+/**
+ * There is a lot of repeated code between this component and FollowWrapper.
+ * This is intentional so that we can implement it as an A/B test.
+ * We will clean this up once the test is complete.
+ */
+import { css } from '@emotion/react';
+import { Topic } from '@guardian/bridget/Topic';
+import { isUndefined, log } from '@guardian/libs';
+import { space, textSans12, textSans15 } from '@guardian/source/foundations';
+import {
+ Button,
+ SvgCheckmark,
+ SvgNotificationsOn,
+ SvgPlus,
+} from '@guardian/source/react-components';
+import {
+ StraightLines,
+ ToggleSwitch,
+} from '@guardian/source-development-kitchen/react-components';
+import { useEffect, useState } from 'react';
+import { getNotificationsClient, getTagClient } from '../lib/bridgetApi';
+import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible';
+import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled';
+import { palette } from '../palette';
+import { isNotInBlockList } from './FollowWrapper.importable';
+
+// -- Follow button --
+
+type FollowButtonProps = {
+ isFollowing: boolean;
+ onClickHandler: () => void;
+};
+
+const FollowButtonPillStyle = ({
+ isFollowing,
+ onClickHandler,
+}: FollowButtonProps) => {
+ return (
+ : }
+ >
+ {isFollowing ? 'Following in My Guardian' : 'Follow'}
+
+ );
+};
+
+// -- notifications --
+
+const notificationAlertStyles = css`
+ ${textSans15}
+ color: ${palette('--follow-text')};
+ min-height: ${space[6]}px;
+ width: 100%;
+`;
+
+const notificationAlertRowStyles = css`
+ display: flex;
+ column-gap: ${space[6]}px;
+ justify-content: space-between;
+ width: 100%;
+`;
+
+const notificationIconStyles = css`
+ display: flex;
+ margin-right: ${space[1]}px;
+
+ svg {
+ margin-top: -${space[1] - 1}px;
+ fill: currentColor;
+ }
+`;
+const notificationLabelStyles = css`
+ display: flex;
+ align-items: flex-start;
+ ${textSans12}
+`;
+
+const NotificationAlert = ({
+ isFollowing,
+ onClickHandler,
+ displayName,
+}: FollowButtonProps & { displayName?: string }) => {
+ return (
+
+
+
+
+
+
+
+ Receive an alert when {displayName ?? 'this author'}{' '}
+ publishes an article
+
+
+
+
+
+
+
+ );
+};
+
+const followBlockStyles = css`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ align-items: flex-start;
+`;
+
+type FollowBlockProps = {
+ contributorId: string;
+ displayName: string;
+};
+
+export const ContributorFollowBlock = ({
+ contributorId,
+ displayName,
+}: FollowBlockProps) => {
+ const [isFollowingNotifications, setIsFollowingNotifications] = useState<
+ boolean | undefined
+ >(undefined);
+ const [isFollowingContributor, setIsFollowingContributor] = useState<
+ boolean | undefined
+ >(undefined);
+
+ const isMyGuardianEnabled = useIsMyGuardianEnabled();
+ const isBridgetCompatible = useIsBridgetCompatible('2.5.0');
+
+ const shouldShowFollow =
+ isBridgetCompatible &&
+ isMyGuardianEnabled &&
+ isNotInBlockList(contributorId);
+
+ useEffect(() => {
+ const topic = new Topic({
+ id: contributorId,
+ displayName,
+ type: 'tag-contributor',
+ });
+
+ void getNotificationsClient()
+ .isFollowing(topic)
+ .then(setIsFollowingNotifications)
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'bridget-getNotificationsClient-isFollowing-error',
+ );
+ log(
+ 'dotcom',
+ 'Bridget getNotificationsClient.isFollowing Error:',
+ error,
+ );
+ });
+
+ void getTagClient()
+ .isFollowing(topic)
+ .then(setIsFollowingContributor)
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'bridget-getTagClient-isFollowing-error',
+ );
+ log('dotcom', 'Bridget getTagClient.isFollowing Error:', error);
+ });
+ }, [contributorId, displayName]);
+
+ const tagHandler = () => {
+ const topic = new Topic({
+ id: contributorId,
+ displayName,
+ type: 'tag-contributor',
+ });
+
+ if (isFollowingContributor) {
+ void getTagClient()
+ .unfollow(topic)
+ .then((success) => {
+ if (success) {
+ setIsFollowingContributor(false);
+ }
+ })
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'bridget-getTagClient-unfollow-error',
+ );
+ log(
+ 'dotcom',
+ 'Bridget getTagClient.unfollow Error:',
+ error,
+ );
+ });
+ } else {
+ void getTagClient()
+ .follow(topic)
+ .then((success) => {
+ if (success) {
+ setIsFollowingContributor(true);
+ }
+ })
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'bridget-getTagClient-follow-error',
+ );
+ log('dotcom', 'Bridget getTagClient.follow Error:', error);
+ });
+ }
+ };
+
+ const notificationsHandler = () => {
+ const topic = new Topic({
+ id: contributorId,
+ displayName,
+ type: 'tag-contributor',
+ });
+
+ if (isFollowingNotifications) {
+ void getNotificationsClient()
+ .unfollow(topic)
+ .then((success) => {
+ if (success) {
+ setIsFollowingNotifications(false);
+ }
+ })
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'briidget-getNotificationsClient-unfollow-error',
+ );
+ log(
+ 'dotcom',
+ 'Bridget getNotificationsClient.unfollow Error:',
+ error,
+ );
+ });
+ } else {
+ void getNotificationsClient()
+ .follow(topic)
+ .then((success) => {
+ if (success) {
+ setIsFollowingNotifications(true);
+ }
+ })
+ .catch((error) => {
+ window.guardian.modules.sentry.reportError(
+ error,
+ 'bridget-getNotificationsClient-follow-error',
+ );
+ log(
+ 'dotcom',
+ 'Bridget getNotificationsClient.follow Error:',
+ error,
+ );
+ });
+ }
+ };
+
+ if (!shouldShowFollow) {
+ return null;
+ }
+
+ return (
+
+
undefined
+ }
+ />
+ {isFollowingContributor && (
+
+
+ undefined
+ }
+ displayName={displayName}
+ />
+
+ )}
+
+ );
+};
diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx
new file mode 100644
index 00000000000..5045b17a521
--- /dev/null
+++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx
@@ -0,0 +1,128 @@
+import { css } from '@emotion/react';
+import {
+ from,
+ headlineBold17,
+ space,
+ textEgyptian14,
+} from '@guardian/source/foundations';
+import { StraightLines } from '@guardian/source-development-kitchen/react-components';
+import sanitise from 'sanitize-html';
+import { palette } from '../palette';
+import { Avatar } from './Avatar';
+import { ContributorFollowBlock } from './ContributorFollowBlock.importable';
+import { Island } from './Island';
+
+type Props = {
+ contributorId: string;
+ displayName: string;
+ avatarUrl?: string;
+ bio?: string;
+};
+
+const contributorBlockStyles = css`
+ display: flex;
+ flex-direction: column;
+ padding: ${space[2]}px ${space[2]}px ${space[2]}px 0;
+`;
+
+const topRowStyles = css`
+ display: flex;
+ gap: ${space[3]}px;
+ margin: ${space[2]}px ${space[3]}px ${space[4]}px 0;
+`;
+
+const avatarContainerStyles = css`
+ width: 60px;
+ height: 60px;
+ flex-shrink: 0;
+
+ ${from.tablet} {
+ width: 80px;
+ height: 80px;
+ }
+`;
+
+const nameAndBioStyles = css`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+`;
+
+const titleStyles = css`
+ ${headlineBold17};
+ color: ${palette('--contributor-follow-fill')};
+ margin: 0 0 ${space[2]}px;
+`;
+
+const bioStyles = css`
+ ${textEgyptian14};
+ font-weight: 500;
+ color: ${palette('--contributor-follow-bio-text')};
+ margin: 0;
+`;
+
+const followButtonContainerStyles = css`
+ display: flex;
+ margin-bottom: ${space[4]}px;
+`;
+
+const containsText = (html: string) => {
+ const htmlWithoutTags = sanitise(html, {
+ allowedTags: [],
+ allowedAttributes: {},
+ });
+ return htmlWithoutTags.length > 0;
+};
+
+export const ContributorFollowBlockComponent = ({
+ contributorId,
+ displayName,
+ avatarUrl,
+ bio,
+}: Props) => {
+ const hasBio = bio !== undefined && containsText(bio);
+ const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined;
+
+ return (
+
+
+
+ {!!avatarUrl && (
+
+ )}
+
+
{displayName}
+ {!!sanitizedBio && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx
new file mode 100644
index 00000000000..de7c695987a
--- /dev/null
+++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx
@@ -0,0 +1,114 @@
+import { css } from '@emotion/react';
+import { mocked } from 'storybook/test';
+import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
+import { getNotificationsClient, getTagClient } from '../lib/bridgetApi';
+import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible';
+import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled';
+import { ContributorFollowBlockComponent } from './ContributorFollowBlockComponent.importable';
+
+const opinionFormat = {
+ display: ArticleDisplay.Standard,
+ design: ArticleDesign.Comment,
+ theme: Pillar.Opinion,
+};
+
+export default {
+ component: ContributorFollowBlockComponent,
+ title: 'Components/ContributorFollowBlockComponent',
+ async beforeEach() {
+ mocked(useIsMyGuardianEnabled).mockReturnValue(true);
+ mocked(useIsBridgetCompatible).mockReturnValue(true);
+ },
+ parameters: {
+ formats: [opinionFormat],
+ },
+};
+
+const contributorData = {
+ contributorId: 'profile/george-monbiot',
+ displayName: 'George Monbiot',
+ avatarUrl:
+ 'https://i.guim.co.uk/img/uploads/2017/10/06/George-Monbiot,-L.png?width=300&quality=85&auto=format&fit=max&s=b4786b498dac53ea736ef05160a2a172',
+ bio: 'George Monbiot is a Guardian columnist and author of The Invisible Doctrine: The Secret History of Neoliberalism
',
+};
+
+const containerStyles = css`
+ max-width: 620px;
+ padding: 16px;
+`;
+
+const mockBridgetClients = (
+ isFollowingTag: boolean,
+ isFollowingNotifications: boolean,
+) => {
+ mocked(getTagClient).mockReturnValue({
+ isFollowing: () => Promise.resolve(isFollowingTag),
+ follow: () => Promise.resolve(true),
+ unfollow: () => Promise.resolve(true),
+ } as unknown as ReturnType);
+ mocked(getNotificationsClient).mockReturnValue({
+ isFollowing: () => Promise.resolve(isFollowingNotifications),
+ follow: () => Promise.resolve(true),
+ unfollow: () => Promise.resolve(true),
+ } as unknown as ReturnType);
+};
+
+const ContributorFollowBlockStory = () => (
+
+
+
+);
+
+export const NotFollowing = () => ;
+NotFollowing.storyName = 'Not Following';
+NotFollowing.beforeEach = () => {
+ mockBridgetClients(false, false);
+};
+
+export const Following = () => ;
+Following.storyName = 'Following';
+Following.beforeEach = () => {
+ mockBridgetClients(true, false);
+};
+
+export const FollowingWithNotifications = () => ;
+FollowingWithNotifications.storyName = 'Following with Notifications';
+FollowingWithNotifications.beforeEach = () => {
+ mockBridgetClients(true, true);
+};
+
+export const AllPillars = () => ;
+AllPillars.storyName = 'All Pillars';
+AllPillars.parameters = {
+ formats: [
+ opinionFormat,
+ {
+ display: ArticleDisplay.Standard,
+ design: ArticleDesign.Comment,
+ theme: Pillar.News,
+ },
+ {
+ display: ArticleDisplay.Standard,
+ design: ArticleDesign.Comment,
+ theme: Pillar.Sport,
+ },
+ {
+ display: ArticleDisplay.Standard,
+ design: ArticleDesign.Comment,
+ theme: Pillar.Culture,
+ },
+ {
+ display: ArticleDisplay.Standard,
+ design: ArticleDesign.Comment,
+ theme: Pillar.Lifestyle,
+ },
+ ],
+};
+AllPillars.beforeEach = () => {
+ mockBridgetClients(false, false);
+};
diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx
index 7208609214c..7bed3a45498 100644
--- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx
+++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx
@@ -13,6 +13,11 @@ type Props = {
displayName: string;
};
+export const isNotInBlockList = (tagId: string) => {
+ const blockList = ['profile/anas-al-sharif'];
+ return !blockList.includes(tagId);
+};
+
export const FollowWrapper = ({ id, displayName }: Props) => {
const [isFollowingNotifications, setIsFollowingNotifications] = useState<
boolean | undefined
@@ -26,11 +31,6 @@ export const FollowWrapper = ({ id, displayName }: Props) => {
const isMyGuardianEnabled = useIsMyGuardianEnabled();
const isBridgetCompatible = useIsBridgetCompatible('2.5.0');
- const isNotInBlockList = (tagId: string) => {
- const blockList = ['profile/anas-al-sharif'];
- return !blockList.includes(tagId);
- };
-
if (isBridgetCompatible && isMyGuardianEnabled && isNotInBlockList(id)) {
setShowFollowTagButton(true);
}
diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json
index 372abaf0599..2f9ed2febed 100644
--- a/dotcom-rendering/src/frontend/schemas/feArticle.json
+++ b/dotcom-rendering/src/frontend/schemas/feArticle.json
@@ -109,6 +109,9 @@
"type": "string"
}
}
+ },
+ "bio": {
+ "type": "string"
}
},
"required": [
@@ -629,6 +632,9 @@
{
"$ref": "#/definitions/ContentAtomBlockElement"
},
+ {
+ "$ref": "#/definitions/ContributorFollowBlockElement"
+ },
{
"$ref": "#/definitions/DisclaimerBlockElement"
},
@@ -1864,6 +1870,36 @@
"elementId"
]
},
+ "ContributorFollowBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.ContributorFollowBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "contributorId": {
+ "type": "string"
+ },
+ "displayName": {
+ "type": "string"
+ },
+ "avatarUrl": {
+ "type": "string"
+ },
+ "bio": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "_type",
+ "contributorId",
+ "displayName",
+ "elementId"
+ ]
+ },
"DisclaimerBlockElement": {
"type": "object",
"properties": {
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx
index 4c7ae4fa39d..ca8bd495407 100644
--- a/dotcom-rendering/src/lib/renderElement.tsx
+++ b/dotcom-rendering/src/lib/renderElement.tsx
@@ -9,6 +9,7 @@ import { CartoonComponent } from '../components/CartoonComponent';
import { ChartAtom } from '../components/ChartAtom.importable';
import { CodeBlockComponent } from '../components/CodeBlockComponent';
import { CommentBlockComponent } from '../components/CommentBlockComponent';
+import { ContributorFollowBlockComponent } from '../components/ContributorFollowBlockComponent.importable';
import { CrosswordComponent } from '../components/CrosswordComponent.importable';
import { DividerBlockComponent } from '../components/DividerBlockComponent';
import { DocumentBlockComponent } from '../components/DocumentBlockComponent.importable';
@@ -285,6 +286,15 @@ export const renderElement = ({
permalink={element.permalink}
/>
);
+ case 'model.dotcomrendering.pageElements.ContributorFollowBlockElement':
+ return (
+
+ );
case 'model.dotcomrendering.pageElements.DividerBlockElement':
return (
+ element._type === 'model.dotcomrendering.pageElements.TextBlockElement';
+
+export const enhanceContributorFollowBlock =
+ (
+ format: ArticleFormat,
+ renderingTarget: RenderingTarget,
+ tags: TagType[] | undefined,
+ byline: string | undefined,
+ ) =>
+ (elements: FEElement[]): FEElement[] => {
+ // TODO: replace with A/B test check
+ const enabled = false;
+ if (!enabled) {
+ return elements;
+ }
+
+ if (renderingTarget !== 'Apps') {
+ return elements;
+ }
+
+ if (format.design !== ArticleDesign.Comment) {
+ return elements;
+ }
+
+ const soleContributor = getSoleContributor(tags ?? [], byline);
+ if (!soleContributor) {
+ return elements;
+ }
+
+ let paragraphCount = 0;
+ let insertPosition: number | null = null;
+
+ for (let i = 0; i < elements.length; i++) {
+ if (isParagraph(elements[i]!)) {
+ paragraphCount++;
+ if (paragraphCount === PARAGRAPH_POSITION) {
+ insertPosition = i + 1;
+ break;
+ }
+ }
+ }
+
+ // If an article has <= 4 paragraphs, insert at the end
+ if (insertPosition === null) {
+ insertPosition = elements.length;
+ }
+
+ const contributorFollowBlockElement: ContributorFollowBlockElement = {
+ _type: 'model.dotcomrendering.pageElements.ContributorFollowBlockElement',
+ elementId: `contributor-profile-${soleContributor.id}`,
+ contributorId: soleContributor.id,
+ displayName: soleContributor.title,
+ avatarUrl: soleContributor.bylineLargeImageUrl,
+ bio: soleContributor.bio,
+ };
+
+ return [
+ ...elements.slice(0, insertPosition),
+ contributorFollowBlockElement,
+ ...elements.slice(insertPosition),
+ ];
+ };
diff --git a/dotcom-rendering/src/model/enhanceBlocks.ts b/dotcom-rendering/src/model/enhanceBlocks.ts
index cdbf26ff205..aaa4bedb472 100644
--- a/dotcom-rendering/src/model/enhanceBlocks.ts
+++ b/dotcom-rendering/src/model/enhanceBlocks.ts
@@ -12,6 +12,7 @@ import type { RenderingTarget } from '../types/renderingTarget';
import type { TagType } from '../types/tag';
import { enhanceAdPlaceholders } from './enhance-ad-placeholders';
import { enhanceBlockquotes } from './enhance-blockquotes';
+import { enhanceContributorFollowBlock } from './enhance-contributor-follow-block';
import { enhanceDisclaimer } from './enhance-disclaimer';
import { enhanceDividers } from './enhance-dividers';
import { enhanceDots } from './enhance-dots';
@@ -40,6 +41,7 @@ type Options = {
pageId: string;
serverSideABTests?: Record;
switches?: Switches;
+ byline?: string;
};
const enhanceNewsletterSignup =
@@ -100,6 +102,12 @@ export const enhanceElements =
options.renderingTarget,
options.shouldHideAds,
),
+ enhanceContributorFollowBlock(
+ format,
+ options.renderingTarget,
+ options.tags,
+ options.byline,
+ ),
enhanceDisclaimer(options.hasAffiliateLinksDisclaimer, isNested),
enhanceProductSummary({
pageId: options.pageId,
diff --git a/dotcom-rendering/src/model/enhanceTags.ts b/dotcom-rendering/src/model/enhanceTags.ts
index 84c857e49ce..5df9b3b2db2 100644
--- a/dotcom-rendering/src/model/enhanceTags.ts
+++ b/dotcom-rendering/src/model/enhanceTags.ts
@@ -11,6 +11,7 @@ const enhanceTag = ({
twitterHandle,
bylineImageUrl,
contributorLargeImagePath: bylineLargeImageUrl,
+ bio,
},
}: FETagType): TagType => ({
id,
@@ -19,4 +20,5 @@ const enhanceTag = ({
twitterHandle,
bylineImageUrl,
bylineLargeImageUrl,
+ bio,
});
diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts
index cc604489aea..61daa0107ae 100644
--- a/dotcom-rendering/src/paletteDeclarations.ts
+++ b/dotcom-rendering/src/paletteDeclarations.ts
@@ -6792,6 +6792,18 @@ const paletteColours = {
light: commentFormInputBackgroundLight,
dark: commentFormInputBackgroundDark,
},
+ '--contributor-follow-fill': {
+ light: followIconFillLight,
+ dark: followIconFillDark,
+ },
+ '--contributor-follow-bio-text': {
+ light: () => sourcePalette.neutral[10],
+ dark: () => sourcePalette.neutral[86],
+ },
+ '--contributor-follow-straight-lines': {
+ light: () => sourcePalette.neutral[86],
+ dark: () => sourcePalette.neutral[38],
+ },
'--cricket-scoreboard-border-top': {
light: cricketScoreboardBorderTop,
dark: cricketScoreboardBorderTop,
diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts
index fce46392341..32bec0b9b73 100644
--- a/dotcom-rendering/src/types/article.ts
+++ b/dotcom-rendering/src/types/article.ts
@@ -106,6 +106,7 @@ export const enhanceArticleType = (
pageId: data.pageId,
serverSideABTests: data.config.serverSideABTests,
switches: data.config.switches,
+ byline: data.byline,
});
const crosswordBlock = buildCrosswordBlock(data);
diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts
index 721ef37ae90..cedc3d2f4a0 100644
--- a/dotcom-rendering/src/types/content.ts
+++ b/dotcom-rendering/src/types/content.ts
@@ -606,6 +606,15 @@ export interface TextBlockElement {
html: string;
}
+export interface ContributorFollowBlockElement {
+ _type: 'model.dotcomrendering.pageElements.ContributorFollowBlockElement';
+ elementId: string;
+ contributorId: string;
+ displayName: string;
+ avatarUrl?: string;
+ bio?: string;
+}
+
export type DCRTimelineEvent = {
date: string;
title?: string;
@@ -832,6 +841,7 @@ export type FEElement =
| CodeBlockElement
| CommentBlockElement
| ContentAtomBlockElement
+ | ContributorFollowBlockElement
| DisclaimerBlockElement
| DividerBlockElement
| DocumentBlockElement
diff --git a/dotcom-rendering/src/types/tag.ts b/dotcom-rendering/src/types/tag.ts
index aca68f30f28..1d4c3534735 100644
--- a/dotcom-rendering/src/types/tag.ts
+++ b/dotcom-rendering/src/types/tag.ts
@@ -57,4 +57,5 @@ export type TagType = {
bylineImageUrl?: string;
bylineLargeImageUrl?: string;
podcast?: Podcast;
+ bio?: string;
};