From 06eef19df1160c153b43e7bf87f5619aeedc9e66 Mon Sep 17 00:00:00 2001
From: Olly <9575458+OllysCoding@users.noreply.github.com>
Date: Tue, 13 Dec 2022 14:58:56 +0000
Subject: [PATCH 01/24] WIP: Start building app infra
---
.../src/app/components/ArticlePage.tsx | 72 ++
.../src/app/layouts/DecideLayout.tsx | 67 ++
.../src/app/layouts/StandardLayout.tsx | 800 ++++++++++++++++++
.../src/app/server/articleToHtml.tsx | 213 +++++
dotcom-rendering/src/app/server/index.ts | 134 +++
.../src/app/server/pageTemplate.ts | 205 +++++
dotcom-rendering/src/server/dev-server.ts | 3 +
7 files changed, 1494 insertions(+)
create mode 100644 dotcom-rendering/src/app/components/ArticlePage.tsx
create mode 100644 dotcom-rendering/src/app/layouts/DecideLayout.tsx
create mode 100644 dotcom-rendering/src/app/layouts/StandardLayout.tsx
create mode 100644 dotcom-rendering/src/app/server/articleToHtml.tsx
create mode 100644 dotcom-rendering/src/app/server/index.ts
create mode 100644 dotcom-rendering/src/app/server/pageTemplate.ts
diff --git a/dotcom-rendering/src/app/components/ArticlePage.tsx b/dotcom-rendering/src/app/components/ArticlePage.tsx
new file mode 100644
index 00000000000..cfe9705d02e
--- /dev/null
+++ b/dotcom-rendering/src/app/components/ArticlePage.tsx
@@ -0,0 +1,72 @@
+import { css, Global } from '@emotion/react';
+import { ArticleDesign } from '@guardian/libs';
+import { brandAlt, focusHalo, neutral } from '@guardian/source-foundations';
+import { StrictMode } from 'react';
+// import { filterABTestSwitches } from '../../model/enhance-switches';
+import type { NavType } from '../../model/extract-nav';
+import type { FEArticleType } from '../../types/frontend';
+import { FetchCommentCounts } from '../../web/components/FetchCommentCounts.importable';
+import { FocusStyles } from '../../web/components/FocusStyles.importable';
+import { Island } from '../../web/components/Island';
+// import { SetABTests } from './SetABTests.importable';
+import { SkipTo } from '../../web/components/SkipTo';
+import { DecideLayout } from '../layouts/DecideLayout';
+
+type Props = {
+ CAPIArticle: FEArticleType;
+ NAV: NavType;
+ format: ArticleFormat;
+};
+
+/**
+ * @description
+ * Article is a high level wrapper for article pages on Dotcom. Sets strict mode and some globals
+ *
+ * @param {Props} props
+ * @param {FEArticleType} props.CAPIArticle - The article JSON data
+ * @param {NAVType} props.NAV - The article JSON data
+ * @param {ArticleFormat} props.format - The format model for the article
+ * */
+export const ArticlePage = ({ CAPIArticle, NAV, format }: Props) => {
+ return (
+
+
+
+ {(format.design === ArticleDesign.LiveBlog ||
+ format.design === ArticleDesign.DeadBlog) && (
+
+ )}
+
+
+
+ {/*
+
+ */}
+
+
+
+ {/*
+
+ */}
+
+
+ );
+};
diff --git a/dotcom-rendering/src/app/layouts/DecideLayout.tsx b/dotcom-rendering/src/app/layouts/DecideLayout.tsx
new file mode 100644
index 00000000000..29fc21ceb81
--- /dev/null
+++ b/dotcom-rendering/src/app/layouts/DecideLayout.tsx
@@ -0,0 +1,67 @@
+import { ArticleDesign, ArticleDisplay } from '@guardian/libs';
+import type { ArticleFormat } from '@guardian/libs';
+import type { NavType } from '../../model/extract-nav';
+import type { FEArticleType } from '../../types/frontend';
+import { StandardLayout } from './StandardLayout';
+
+type Props = {
+ CAPIArticle: FEArticleType;
+ NAV: NavType;
+ format: ArticleFormat;
+};
+
+export const DecideLayout = ({ CAPIArticle, NAV, format }: Props) => {
+ switch (format.display) {
+ case ArticleDisplay.Immersive: {
+ switch (format.design) {
+ case ArticleDesign.Interactive: {
+ return
Not Supported
;
+ }
+ default: {
+ return Not Supported
;
+ }
+ }
+ }
+ case ArticleDisplay.NumberedList:
+ case ArticleDisplay.Showcase: {
+ switch (format.design) {
+ case ArticleDesign.LiveBlog:
+ case ArticleDesign.DeadBlog:
+ return Not Supported
;
+ case ArticleDesign.Comment:
+ case ArticleDesign.Editorial:
+ case ArticleDesign.Letter:
+ return Not Supported
;
+ default:
+ return Not Supported
;
+ }
+ }
+ case ArticleDisplay.Standard:
+ default: {
+ switch (format.design) {
+ case ArticleDesign.Interactive:
+ return Not Supported
;
+ case ArticleDesign.FullPageInteractive: {
+ return Not Supported
;
+ }
+ case ArticleDesign.LiveBlog:
+ case ArticleDesign.DeadBlog:
+ return Not Supported
;
+ case ArticleDesign.Comment:
+ case ArticleDesign.Editorial:
+ case ArticleDesign.Letter:
+ return Not Supported
;
+ case ArticleDesign.NewsletterSignup:
+ return Not Supported
;
+ default:
+ return (
+
+ );
+ }
+ }
+ }
+};
diff --git a/dotcom-rendering/src/app/layouts/StandardLayout.tsx b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
new file mode 100644
index 00000000000..b69cbd822b6
--- /dev/null
+++ b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
@@ -0,0 +1,800 @@
+import { css } from '@emotion/react';
+import { ArticleDesign, ArticleSpecial } from '@guardian/libs';
+import type { ArticleFormat } from '@guardian/libs';
+import {
+ border,
+ brandAltBackground,
+ from,
+ labs,
+ until,
+} from '@guardian/source-foundations';
+import { StraightLines } from '@guardian/source-react-components-development-kitchen';
+import type { NavType } from '../../model/extract-nav';
+import type { FEArticleType } from '../../types/frontend';
+import { ArticleBody } from '../../web/components/ArticleBody';
+import { ArticleContainer } from '../../web/components/ArticleContainer';
+import { ArticleHeadline } from '../../web/components/ArticleHeadline';
+import { ArticleMeta } from '../../web/components/ArticleMeta';
+import { ArticleTitle } from '../../web/components/ArticleTitle';
+import { Border } from '../../web/components/Border';
+import { Carousel } from '../../web/components/Carousel.importable';
+import { DecideLines } from '../../web/components/DecideLines';
+import { DecideOnwards } from '../../web/components/DecideOnwards';
+import { GetMatchNav } from '../../web/components/GetMatchNav.importable';
+import { GetMatchStats } from '../../web/components/GetMatchStats.importable';
+import { GetMatchTabs } from '../../web/components/GetMatchTabs.importable';
+import { GridItem } from '../../web/components/GridItem';
+import { GuardianLabsLines } from '../../web/components/GuardianLabsLines';
+import { Island } from '../../web/components/Island';
+import { LabsHeader } from '../../web/components/LabsHeader.importable';
+import { MainMedia } from '../../web/components/MainMedia';
+import { MostViewedFooterData } from '../../web/components/MostViewedFooterData.importable';
+import { MostViewedFooterLayout } from '../../web/components/MostViewedFooterLayout';
+import { MostViewedRightWrapper } from '../../web/components/MostViewedRightWrapper.importable';
+import { OnwardsUpper } from '../../web/components/OnwardsUpper.importable';
+import { RightColumn } from '../../web/components/RightColumn';
+import { Section } from '../../web/components/Section';
+import { Standfirst } from '../../web/components/Standfirst';
+import { StarRating } from '../../web/components/StarRating/StarRating';
+import { SubMeta } from '../../web/components/SubMeta';
+import { TableOfContents } from '../../web/components/TableOfContents';
+import { Stuck } from '../../web/layouts/lib/stickiness';
+import { decidePalette } from '../../web/lib/decidePalette';
+import { decideTrail } from '../../web/lib/decideTrail';
+
+const StandardGrid = ({
+ children,
+ isMatchReport,
+}: {
+ children: React.ReactNode;
+ isMatchReport: boolean;
+}) => (
+
+ {children}
+
+);
+
+const maxWidth = css`
+ ${from.desktop} {
+ max-width: 620px;
+ }
+`;
+
+const stretchLines = css`
+ ${until.phablet} {
+ margin-left: -20px;
+ margin-right: -20px;
+ }
+ ${until.mobileLandscape} {
+ margin-left: -10px;
+ margin-right: -10px;
+ }
+`;
+
+const starWrapper = css`
+ margin-bottom: 18px;
+ margin-top: 6px;
+ background-color: ${brandAltBackground.primary};
+ display: inline-block;
+
+ ${until.phablet} {
+ padding-left: 20px;
+ margin-left: -20px;
+ }
+ ${until.leftCol} {
+ padding-left: 0px;
+ margin-left: -0px;
+ }
+
+ padding-left: 10px;
+ margin-left: -10px;
+`;
+
+interface Props {
+ CAPIArticle: FEArticleType;
+ NAV: NavType;
+ format: ArticleFormat;
+}
+
+export const StandardLayout = ({ CAPIArticle, NAV, format }: Props) => {
+ const {
+ config: { isPaidContent, host },
+ } = CAPIArticle;
+
+ const footballMatchUrl =
+ CAPIArticle.matchType === 'FootballMatchType' && CAPIArticle.matchUrl;
+
+ const isMatchReport =
+ format.design === ArticleDesign.MatchReport && !!footballMatchUrl;
+
+ const showComments = CAPIArticle.isCommentable;
+
+ // const { branding } =
+ // CAPIArticle.commercialProperties[CAPIArticle.editionId];
+
+ const palette = decidePalette(format);
+
+ // const contributionsServiceUrl = getContributionsServiceUrl(CAPIArticle);
+
+ /**
+ * This property currently only applies to the header and merchandising slots
+ */
+ // const renderAds = !CAPIArticle.isAdFreeUser && !CAPIArticle.shouldHideAds;
+
+ return (
+ <>
+ {/* TODO: Review this because AR didn't previously support labs articles */}
+ {format.theme === ArticleSpecial.Labs && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {format.theme === ArticleSpecial.Labs ? (
+ <>>
+ ) : (
+
+ )}
+
+
+
+ {isMatchReport && (
+
+
+
+ )}
+
+
+
+
+ {isMatchReport && (
+
+
+
+ )}
+
+
+
+
+ {CAPIArticle.starRating ||
+ CAPIArticle.starRating === 0 ? (
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {format.theme === ArticleSpecial.Labs ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {CAPIArticle.tableOfContents && (
+
+ )}
+
+ {format.design === ArticleDesign.MatchReport &&
+ !!footballMatchUrl && (
+
+
+
+ )}
+
+ {/* We know we need something like this but we don't know what yet */}
+ {/* {showBodyEndSlot && (
+
+
+
+ )} */}
+
+
+
+
+
+
+
+ {/* Todo: make this an 'app ad' */}
+ {/* {!CAPIArticle.shouldHideAds && (
+
+ )} */}
+ {!isPaidContent ? (
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+
+ {/* Todo: do we want this? */}
+ {/* {renderAds && !isLabs && (
+
+ )} */}
+
+ {CAPIArticle.onwards ? (
+
+ ) : (
+ <>
+ {CAPIArticle.storyPackage && (
+
+ )}
+
+
+
+
+ >
+ )}
+
+ {/* TODO: Review if we can get discussion working */}
+ {/* {!isPaidContent && showComments && (
+
+ )} */}
+
+ {!isPaidContent && (
+
+ )}
+
+ {/* Todo: review if we need this */}
+ {/* {renderAds && !isLabs && (
+
+ )} */}
+
+ {/*(*/}
+ >
+ );
+};
diff --git a/dotcom-rendering/src/app/server/articleToHtml.tsx b/dotcom-rendering/src/app/server/articleToHtml.tsx
new file mode 100644
index 00000000000..bc657fcad47
--- /dev/null
+++ b/dotcom-rendering/src/app/server/articleToHtml.tsx
@@ -0,0 +1,213 @@
+import createCache from '@emotion/cache';
+import { CacheProvider } from '@emotion/react';
+import createEmotionServer from '@emotion/server/create-instance';
+import { ArticleDesign, ArticlePillar } from '@guardian/libs';
+import { renderToString } from 'react-dom/server';
+import {
+ BUILD_VARIANT,
+ dcrJavascriptBundle,
+} from '../../../scripts/webpack/bundles';
+import {
+ ASSET_ORIGIN,
+ generateScriptTags,
+ getScriptsFromManifest,
+} from '../../lib/assets';
+import { extractNAV } from '../../model/extract-nav';
+// import { makeWindowGuardian } from '../../model/window-guardian';
+import type { CAPIElement } from '../../types/content';
+import type { FEArticleType } from '../../types/frontend';
+import { ArticlePage } from '../../web/components/ArticlePage';
+import { decideFormat } from '../../web/lib/decideFormat';
+import { decideTheme } from '../../web/lib/decideTheme';
+import { getHttp3Url } from '../../web/lib/getHttp3Url';
+import { pageTemplate } from './pageTemplate';
+
+interface Props {
+ article: FEArticleType;
+}
+
+const decideTitle = (article: FEArticleType): string => {
+ if (
+ decideTheme(article.format) === ArticlePillar.Opinion &&
+ article.byline
+ ) {
+ return `${article.headline} | ${article.byline} | The Guardian`;
+ }
+ return `${article.headline} | ${article.sectionLabel} | The Guardian`;
+};
+
+export const articleToHtml = ({ article }: Props): string => {
+ const NAV = extractNAV(article.nav);
+ const title = decideTitle(article);
+ const key = 'ar';
+ const cache = createCache({ key });
+
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ const { extractCriticalToChunks, constructStyleTagsFromChunks } =
+ createEmotionServer(cache);
+
+ const format: ArticleFormat = decideFormat(article.format);
+
+ const html = renderToString(
+
+
+ ,
+ );
+
+ const chunks = extractCriticalToChunks(html);
+ const extractedCss = constructStyleTagsFromChunks(chunks);
+
+ // We want to only insert script tags for the elements or main media elements on this page view
+ // so we need to check what elements we have and use the mapping to the the chunk name
+ const CAPIElements: CAPIElement[] = article.blocks
+ .map((block) => block.elements)
+ .flat();
+
+ // Evaluating the performance of HTTP3 over HTTP2
+ // See: https://github.com/guardian/dotcom-rendering/pull/5394
+ const { offerHttp3 = false } = article.config.switches;
+
+ const pageHasNonBootInteractiveElements = CAPIElements.some(
+ (element) =>
+ element._type ===
+ 'model.dotcomrendering.pageElements.InteractiveBlockElement' &&
+ element.scriptUrl !==
+ 'https://interactive.guim.co.uk/embed/iframe-wrapper/0.1/boot.js', // We have rewritten this standard behaviour into Dotcom Rendering
+ );
+
+ const pageHasTweetElements = CAPIElements.some(
+ (element) =>
+ element._type ===
+ 'model.dotcomrendering.pageElements.TweetBlockElement',
+ );
+
+ const shouldServeVariantBundle: boolean = [
+ BUILD_VARIANT,
+ article.config.abTests[dcrJavascriptBundle('Variant')] === 'variant',
+ ].every(Boolean);
+
+ /**
+ * This function returns an array of files found in the manifests
+ * defined by `manifestPaths`.
+ *
+ * @see getScriptsFromManifest
+ */
+ const getScriptArrayFromFile = getScriptsFromManifest(
+ shouldServeVariantBundle,
+ );
+
+ /**
+ * The highest priority scripts.
+ * These scripts have a considerable impact on site performance.
+ * Only scripts critical to application execution may go in here.
+ * Please talk to the dotcom platform team before adding more.
+ * Scripts will be executed in the order they appear in this array
+ */
+ const priorityScriptTags = generateScriptTags(
+ [
+ ...getScriptArrayFromFile('bootCmp.js'),
+ ...getScriptArrayFromFile('ophan.js'),
+ process.env.COMMERCIAL_BUNDLE_URL ??
+ article.config.commercialBundleUrl,
+ ...getScriptArrayFromFile('sentryLoader.js'),
+ ...getScriptArrayFromFile('dynamicImport.js'),
+ pageHasNonBootInteractiveElements &&
+ `${ASSET_ORIGIN}static/frontend/js/curl-with-js-and-domReady.js`,
+ ...getScriptArrayFromFile('islands.js'),
+ ].map((script) =>
+ offerHttp3 && script ? getHttp3Url(script) : script,
+ ),
+ );
+
+ /**
+ * Low priority scripts. These scripts will be requested
+ * asynchronously after the main HTML has been parsed. Execution
+ * order is not guaranteed. It is even possible that these execute
+ * *before* the high priority scripts, although this is very
+ * unlikely.
+ */
+ const lowPriorityScriptTags = generateScriptTags(
+ [
+ ...getScriptArrayFromFile('atomIframe.js'),
+ ...getScriptArrayFromFile('embedIframe.js'),
+ ...getScriptArrayFromFile('newsletterEmbedIframe.js'),
+ ...getScriptArrayFromFile('relativeTime.js'),
+ ...getScriptArrayFromFile('initDiscussion.js'),
+ ].map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
+ );
+
+ /**
+ * We escape windowGuardian here to prevent errors when the data
+ * is placed in a script tag on the page
+ */
+ // const windowGuardian = escapeData(
+ // JSON.stringify(
+ // makeWindowGuardian({
+ // editionId: article.editionId,
+ // stage: article.config.stage,
+ // frontendAssetsFullURL: article.config.frontendAssetsFullURL,
+ // revisionNumber: article.config.revisionNumber,
+ // sentryPublicApiKey: article.config.sentryPublicApiKey,
+ // sentryHost: article.config.sentryHost,
+ // keywordIds: article.config.keywordIds,
+ // dfpAccountId: article.config.dfpAccountId,
+ // adUnit: article.config.adUnit,
+ // ajaxUrl: article.config.ajaxUrl,
+ // googletagUrl: article.config.googletagUrl,
+ // switches: article.config.switches,
+ // abTests: article.config.abTests,
+ // brazeApiKey: article.config.brazeApiKey,
+ // isPaidContent: article.pageType.isPaidContent,
+ // contentType: article.contentType,
+ // shouldHideReaderRevenue: article.shouldHideReaderRevenue,
+ // GAData: extractGA({
+ // webTitle: article.webTitle,
+ // format: article.format,
+ // sectionName: article.sectionName,
+ // contentType: article.contentType,
+ // tags: article.tags,
+ // pageId: article.pageId,
+ // editionId: article.editionId,
+ // beaconURL: article.beaconURL,
+ // }),
+ // unknownConfig: article.config,
+ // }),
+ // ),
+ // );
+
+ const initTwitter = `
+`;
+
+ return pageTemplate({
+ priorityScriptTags,
+ lowPriorityScriptTags,
+ css: extractedCss,
+ html,
+ title,
+ description: article.trailText,
+ // windowGuardian,
+ initTwitter:
+ pageHasTweetElements || format.design === ArticleDesign.LiveBlog
+ ? initTwitter
+ : undefined,
+ offerHttp3,
+ });
+};
diff --git a/dotcom-rendering/src/app/server/index.ts b/dotcom-rendering/src/app/server/index.ts
new file mode 100644
index 00000000000..c25fabc22d9
--- /dev/null
+++ b/dotcom-rendering/src/app/server/index.ts
@@ -0,0 +1,134 @@
+import type { RequestHandler } from 'express';
+import { Standard as ExampleArticle } from '../../../fixtures/generated/articles/Standard';
+import { isRecipe } from '../../model/enhance-recipes';
+import { enhanceBlocks } from '../../model/enhanceBlocks';
+import { enhanceCommercialProperties } from '../../model/enhanceCommercialProperties';
+import { enhanceStandfirst } from '../../model/enhanceStandfirst';
+import { enhanceTableOfContents } from '../../model/enhanceTableOfContents';
+import { validateAsCAPIType } from '../../model/validate';
+import type { FEArticleType } from '../../types/frontend';
+import { articleToHtml } from './articleToHtml';
+
+function enhancePinnedPost(format: CAPIFormat, block?: Block) {
+ return block ? enhanceBlocks([block], format)[0] : block;
+}
+
+const enhanceCAPIType = (body: unknown): FEArticleType => {
+ const data = validateAsCAPIType(body);
+
+ const enhancedBlocks = enhanceBlocks(data.blocks, data.format, {
+ promotedNewsletter: data.promotedNewsletter,
+ isRecipe: isRecipe(data.tags),
+ });
+
+ const CAPIArticle: FEArticleType = {
+ ...data,
+ blocks: enhancedBlocks,
+ pinnedPost: enhancePinnedPost(data.format, data.pinnedPost),
+ standfirst: enhanceStandfirst(data.standfirst),
+ commercialProperties: enhanceCommercialProperties(
+ data.commercialProperties,
+ ),
+ tableOfContents: data.config.switches.tableOfContents
+ ? enhanceTableOfContents(data.format, enhancedBlocks)
+ : undefined,
+ };
+ return CAPIArticle;
+};
+
+const getStack = (e: unknown): string =>
+ e instanceof Error ? e.stack ?? 'No error stack' : 'Unknown error';
+
+export const handleArticle: RequestHandler = ({ body }, res) => {
+ try {
+ const article = enhanceCAPIType(body);
+ const resp = articleToHtml({
+ article,
+ });
+
+ res.status(200).send(resp);
+ } catch (e) {
+ res.status(500).send(`${getStack(e)}`);
+ }
+};
+
+export const handleArticleJson: RequestHandler = ({ body }, res) => {
+ try {
+ const CAPIArticle = enhanceCAPIType(body);
+ const resp = {
+ data: {
+ CAPIArticle,
+ },
+ };
+
+ res.status(200).send(resp);
+ } catch (e) {
+ res.status(500).send(`${getStack(e)}`);
+ }
+};
+
+export const handlePerfTest: RequestHandler = (req, res, next) => {
+ req.body = ExampleArticle;
+ handleArticle(req, res, next);
+};
+
+export const handleInteractive: RequestHandler = ({ body }, res) => {
+ try {
+ const article = enhanceCAPIType(body);
+ const resp = articleToHtml({
+ article,
+ });
+
+ res.status(200).send(resp);
+ } catch (e) {
+ res.status(500).send(`${getStack(e)}`);
+ }
+};
+
+// We might need this for liveblogs
+// export const handleBlocks: RequestHandler = ({ body }, res) => {
+// try {
+// const {
+// blocks,
+// format,
+// host,
+// pageId,
+// webTitle,
+// ajaxUrl,
+// isAdFreeUser,
+// isSensitive,
+// videoDuration,
+// edition,
+// section,
+// sharedAdTargeting,
+// adUnit,
+// switches,
+// keywordIds,
+// } =
+// // The content if body is not checked
+// body as BlocksRequest;
+
+// const enhancedBlocks = enhanceBlocks(blocks, format);
+// const html = blocksToHtml({
+// blocks: enhancedBlocks,
+// format,
+// host,
+// pageId,
+// webTitle,
+// ajaxUrl,
+// isAdFreeUser,
+// isSensitive,
+// videoDuration,
+// edition,
+// section,
+// sharedAdTargeting,
+// adUnit,
+// switches,
+// keywordIds,
+// });
+
+// res.status(200).send(html);
+// } catch (e) {
+// res.status(500).send(`${getStack(e)}`);
+// }
+// };
diff --git a/dotcom-rendering/src/app/server/pageTemplate.ts b/dotcom-rendering/src/app/server/pageTemplate.ts
new file mode 100644
index 00000000000..9e0d303da5f
--- /dev/null
+++ b/dotcom-rendering/src/app/server/pageTemplate.ts
@@ -0,0 +1,205 @@
+import { brandBackground, resets } from '@guardian/source-foundations';
+import he from 'he';
+import { ASSET_ORIGIN } from '../../lib/assets';
+import { getFontsCss } from '../../lib/fonts-css';
+import { getHttp3Url } from '../../web/lib/getHttp3Url';
+
+export const pageTemplate = ({
+ css,
+ html,
+ // windowGuardian,
+ priorityScriptTags,
+ lowPriorityScriptTags,
+ offerHttp3,
+ title = 'The Guardian',
+ description = 'Latest news, sport, business, comment, analysis and reviews from the Guardian, the world's leading liberal voice',
+ initTwitter,
+}: {
+ css: string;
+ html: string;
+ // windowGuardian: string;
+ priorityScriptTags: string[];
+ lowPriorityScriptTags: string[];
+ offerHttp3: boolean;
+ title?: string;
+ description?: string;
+ initTwitter?: string;
+}): string => {
+ const favicon =
+ process.env.NODE_ENV === 'production'
+ ? 'favicon-32x32.ico'
+ : 'favicon-32x32-dev-yellow.ico';
+
+ /**
+ * Preload the following woff2 font files
+ * TODO: Identify critical fonts to preload
+ */
+ const fontFiles = [
+ // 'https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/noalts-not-hinted/GHGuardianHeadline-Light.woff2',
+ // 'https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/noalts-not-hinted/GHGuardianHeadline-LightItalic.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/noalts-not-hinted/GHGuardianHeadline-Medium.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/noalts-not-hinted/GHGuardianHeadline-MediumItalic.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/noalts-not-hinted/GHGuardianHeadline-Bold.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/noalts-not-hinted/GuardianTextEgyptian-Regular.woff2',
+ // 'https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/noalts-not-hinted/GuardianTextEgyptian-RegularItalic.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/noalts-not-hinted/GuardianTextEgyptian-Bold.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-textsans/noalts-not-hinted/GuardianTextSans-Regular.woff2',
+ // 'http://assets.guim.co.uk/static/frontend/fonts/guardian-textsans/noalts-not-hinted/GuardianTextSans-RegularItalic.woff2',
+ 'https://assets.guim.co.uk/static/frontend/fonts/guardian-textsans/noalts-not-hinted/GuardianTextSans-Bold.woff2',
+ ].map((font) => (offerHttp3 ? getHttp3Url(font) : font));
+
+ const fontPreloadTags = fontFiles.map(
+ (fontFile) =>
+ ``,
+ );
+
+ // Opt out of having information from our website used for personalization of content and suggestions for Twitter users, including ads
+ // See https://developer.twitter.com/en/docs/twitter-for-websites/webpage-properties/overview
+ const twitterSecAndPrivacyMetaTags = ``;
+
+ // Duplicated prefetch and preconnect tags from DCP:
+ // Documented here: https://github.com/guardian/frontend/pull/12935
+ // Preconnect should be used for the most crucial third party domains
+ // "use preconnect when you know for sure that you’re going to be accessing a resource"
+ // - https://www.smashingmagazine.com/2019/04/optimization-performance-resource-hints/
+ // DNS-prefetch should be used for other third party domains that we are likely to connect to but not sure (ads)
+ // Preconnecting to too many URLs can reduce page performance
+ // DNS-prefetch can also be used as a fallback for IE11
+ // More information on preconnecting:
+ // https://css-tricks.com/using-relpreconnect-to-establish-network-connections-early-and-increase-performance/
+ // More information on prefetching:
+ // https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch
+ const staticPreconnectUrls = [
+ `${ASSET_ORIGIN}`,
+ `https://i.guim.co.uk`,
+ `https://j.ophan.co.uk`,
+ `https://ophan.theguardian.com`,
+ ];
+
+ const staticPrefetchUrls = [
+ ...staticPreconnectUrls,
+ `https://api.nextgen.guardianapps.co.uk`,
+ `https://hits-secure.theguardian.com`,
+ `https://interactive.guim.co.uk`,
+ `https://phar.gu-web.net`,
+ `https://static.theguardian.com`,
+ `https://support.theguardian.com`,
+ ];
+
+ const allStaticPreconnectUrls =
+ process.env.NODE_ENV === 'production'
+ ? [...staticPreconnectUrls, 'https://sourcepoint.theguardian.com']
+ : staticPreconnectUrls;
+
+ const preconnectTags = allStaticPreconnectUrls.map(
+ (src) => ``,
+ );
+
+ const prefetchTags = staticPrefetchUrls.map(
+ (src) => ``,
+ );
+
+ const weAreHiringMessage = `
+`;
+
+ return `
+
+
+ ${weAreHiringMessage}
+ ${title}
+
+
+
+
+
+
+
+ ${preconnectTags.join('\n')}
+ ${prefetchTags.join('\n')}
+
+
+ ${fontPreloadTags.join('\n')}
+
+ ${twitterSecAndPrivacyMetaTags}
+
+
+
+
+
+
+
+
+
+
+ ${initTwitter ?? ''}
+
+ ${priorityScriptTags.join('\n')}
+
+
+ ${css}
+
+
+
+
+ ${html}
+ ${[...lowPriorityScriptTags].join('\n')}
+
+ `;
+};
diff --git a/dotcom-rendering/src/server/dev-server.ts b/dotcom-rendering/src/server/dev-server.ts
index 7bd55544a53..03c2b0268e0 100644
--- a/dotcom-rendering/src/server/dev-server.ts
+++ b/dotcom-rendering/src/server/dev-server.ts
@@ -1,5 +1,6 @@
import type { Handler } from 'express';
import { handleAMPArticle } from '../amp/server';
+import { handleArticle as handleAppArticle } from '../app/server';
import {
handleArticle,
handleArticleJson,
@@ -38,6 +39,8 @@ export const devServer = (): Handler => {
return handleFront(req, res, next);
case '/FrontJSON':
return handleFrontJson(req, res, next);
+ case '/app/Article':
+ return handleAppArticle(req, res, next);
default: {
if (req.url.match(ARTICLE_URL)) {
const url = new URL(
From 522af70b2335cb23646068fdc1822a3b385ec367 Mon Sep 17 00:00:00 2001
From: Jamie B <53781962+JamieB-gu@users.noreply.github.com>
Date: Wed, 14 Dec 2022 15:28:38 +0000
Subject: [PATCH 02/24] Add `Platform` enum
---
.../src/app/components/ArticlePage.tsx | 6 +-
.../src/app/layouts/DecideLayout.tsx | 5 +-
.../src/app/layouts/StandardLayout.tsx | 9 +--
.../src/app/server/articleToHtml.tsx | 6 +-
dotcom-rendering/src/types/platform.ts | 6 ++
.../src/web/components/ArticleBody.tsx | 77 +++++++++++--------
.../src/web/components/LiveBlock.tsx | 2 +-
.../src/web/lib/LiveBlogRenderer.tsx | 73 ++++++++++--------
.../src/web/server/blocksToHtml.tsx | 1 +
9 files changed, 101 insertions(+), 84 deletions(-)
create mode 100644 dotcom-rendering/src/types/platform.ts
diff --git a/dotcom-rendering/src/app/components/ArticlePage.tsx b/dotcom-rendering/src/app/components/ArticlePage.tsx
index cfe9705d02e..bf74b06d176 100644
--- a/dotcom-rendering/src/app/components/ArticlePage.tsx
+++ b/dotcom-rendering/src/app/components/ArticlePage.tsx
@@ -3,7 +3,6 @@ import { ArticleDesign } from '@guardian/libs';
import { brandAlt, focusHalo, neutral } from '@guardian/source-foundations';
import { StrictMode } from 'react';
// import { filterABTestSwitches } from '../../model/enhance-switches';
-import type { NavType } from '../../model/extract-nav';
import type { FEArticleType } from '../../types/frontend';
import { FetchCommentCounts } from '../../web/components/FetchCommentCounts.importable';
import { FocusStyles } from '../../web/components/FocusStyles.importable';
@@ -14,7 +13,6 @@ import { DecideLayout } from '../layouts/DecideLayout';
type Props = {
CAPIArticle: FEArticleType;
- NAV: NavType;
format: ArticleFormat;
};
@@ -27,7 +25,7 @@ type Props = {
* @param {NAVType} props.NAV - The article JSON data
* @param {ArticleFormat} props.format - The format model for the article
* */
-export const ArticlePage = ({ CAPIArticle, NAV, format }: Props) => {
+export const ArticlePage = ({ CAPIArticle, format }: Props) => {
return (
{
isDev={!!CAPIArticle.config.isDev}
/>
*/}
-
+
);
};
diff --git a/dotcom-rendering/src/app/layouts/DecideLayout.tsx b/dotcom-rendering/src/app/layouts/DecideLayout.tsx
index 29fc21ceb81..28aae16abd3 100644
--- a/dotcom-rendering/src/app/layouts/DecideLayout.tsx
+++ b/dotcom-rendering/src/app/layouts/DecideLayout.tsx
@@ -1,16 +1,14 @@
import { ArticleDesign, ArticleDisplay } from '@guardian/libs';
import type { ArticleFormat } from '@guardian/libs';
-import type { NavType } from '../../model/extract-nav';
import type { FEArticleType } from '../../types/frontend';
import { StandardLayout } from './StandardLayout';
type Props = {
CAPIArticle: FEArticleType;
- NAV: NavType;
format: ArticleFormat;
};
-export const DecideLayout = ({ CAPIArticle, NAV, format }: Props) => {
+export const DecideLayout = ({ CAPIArticle, format }: Props) => {
switch (format.display) {
case ArticleDisplay.Immersive: {
switch (format.design) {
@@ -57,7 +55,6 @@ export const DecideLayout = ({ CAPIArticle, NAV, format }: Props) => {
return (
);
diff --git a/dotcom-rendering/src/app/layouts/StandardLayout.tsx b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
index b69cbd822b6..5f8a44e24f3 100644
--- a/dotcom-rendering/src/app/layouts/StandardLayout.tsx
+++ b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
@@ -9,7 +9,6 @@ import {
until,
} from '@guardian/source-foundations';
import { StraightLines } from '@guardian/source-react-components-development-kitchen';
-import type { NavType } from '../../model/extract-nav';
import type { FEArticleType } from '../../types/frontend';
import { ArticleBody } from '../../web/components/ArticleBody';
import { ArticleContainer } from '../../web/components/ArticleContainer';
@@ -274,11 +273,10 @@ const starWrapper = css`
interface Props {
CAPIArticle: FEArticleType;
- NAV: NavType;
format: ArticleFormat;
}
-export const StandardLayout = ({ CAPIArticle, NAV, format }: Props) => {
+export const StandardLayout = ({ CAPIArticle, format }: Props) => {
const {
config: { isPaidContent, host },
} = CAPIArticle;
@@ -289,7 +287,7 @@ export const StandardLayout = ({ CAPIArticle, NAV, format }: Props) => {
const isMatchReport =
format.design === ArticleDesign.MatchReport && !!footballMatchUrl;
- const showComments = CAPIArticle.isCommentable;
+ // const showComments = CAPIArticle.isCommentable;
// const { branding } =
// CAPIArticle.commercialProperties[CAPIArticle.editionId];
@@ -428,7 +426,6 @@ export const StandardLayout = ({ CAPIArticle, NAV, format }: Props) => {
{
{
format={format}
blocks={CAPIArticle.blocks}
pinnedPost={CAPIArticle.pinnedPost}
- adTargeting={adTargeting}
host={host}
pageId={CAPIArticle.pageId}
webTitle={CAPIArticle.webTitle}
diff --git a/dotcom-rendering/src/app/server/articleToHtml.tsx b/dotcom-rendering/src/app/server/articleToHtml.tsx
index bc657fcad47..69e2325d776 100644
--- a/dotcom-rendering/src/app/server/articleToHtml.tsx
+++ b/dotcom-rendering/src/app/server/articleToHtml.tsx
@@ -12,11 +12,10 @@ import {
generateScriptTags,
getScriptsFromManifest,
} from '../../lib/assets';
-import { extractNAV } from '../../model/extract-nav';
// import { makeWindowGuardian } from '../../model/window-guardian';
import type { CAPIElement } from '../../types/content';
import type { FEArticleType } from '../../types/frontend';
-import { ArticlePage } from '../../web/components/ArticlePage';
+import { ArticlePage } from '../components/ArticlePage';
import { decideFormat } from '../../web/lib/decideFormat';
import { decideTheme } from '../../web/lib/decideTheme';
import { getHttp3Url } from '../../web/lib/getHttp3Url';
@@ -37,7 +36,6 @@ const decideTitle = (article: FEArticleType): string => {
};
export const articleToHtml = ({ article }: Props): string => {
- const NAV = extractNAV(article.nav);
const title = decideTitle(article);
const key = 'ar';
const cache = createCache({ key });
@@ -50,7 +48,7 @@ export const articleToHtml = ({ article }: Props): string => {
const html = renderToString(
-
+
,
);
diff --git a/dotcom-rendering/src/types/platform.ts b/dotcom-rendering/src/types/platform.ts
new file mode 100644
index 00000000000..e52a113f950
--- /dev/null
+++ b/dotcom-rendering/src/types/platform.ts
@@ -0,0 +1,6 @@
+enum Platform {
+ Web,
+ AMP,
+ Apps,
+ Editions,
+}
diff --git a/dotcom-rendering/src/web/components/ArticleBody.tsx b/dotcom-rendering/src/web/components/ArticleBody.tsx
index 292fdd4a1f2..187ac67693b 100644
--- a/dotcom-rendering/src/web/components/ArticleBody.tsx
+++ b/dotcom-rendering/src/web/components/ArticleBody.tsx
@@ -13,11 +13,11 @@ import { revealStyles } from '../lib/revealStyles';
import { Island } from './Island';
import { RecipeMultiplier } from './RecipeMultiplier.importable';
-type Props = {
+type CommonProps = {
format: ArticleFormat;
blocks: Block[];
pinnedPost?: Block;
- adTargeting: AdTargeting;
+ adTargeting?: AdTargeting;
host?: string;
pageId: string;
webTitle: string;
@@ -28,7 +28,6 @@ type Props = {
shouldHideReaderRevenue: boolean;
tags: TagType[];
isPaidContent: boolean;
- contributionsServiceUrl: string;
contentType: string;
sectionName: string;
keywordIds: string;
@@ -43,6 +42,17 @@ type Props = {
selectedTopics?: Topic[];
};
+type AppsProps = {
+ platform: Platform.Apps;
+}
+
+type WebProps = {
+ platform: Platform.Web;
+ contributionsServiceUrl: string;
+}
+
+type Props = CommonProps & (AppsProps | WebProps);
+
const globalH2Styles = (display: ArticleDisplay) => css`
h2:not([data-ignore='global-h2-styling']) {
${display === ArticleDisplay.Immersive
@@ -107,35 +117,36 @@ const globalLinkStyles = (palette: Palette) => css`
}
`;
-export const ArticleBody = ({
- format,
- blocks,
- pinnedPost,
- adTargeting,
- host,
- pageId,
- webTitle,
- ajaxUrl,
- switches,
- isAdFreeUser,
- section,
- shouldHideReaderRevenue,
- tags,
- isPaidContent,
- contributionsServiceUrl,
- contentType,
- sectionName,
- isPreview,
- idUrl,
- isSensitive,
- isDev,
- onFirstPage,
- keyEvents,
- filterKeyEvents,
- availableTopics,
- selectedTopics,
- keywordIds,
-}: Props) => {
+export const ArticleBody = (props: Props) => {
+ const {
+ format,
+ blocks,
+ pinnedPost,
+ adTargeting,
+ host,
+ pageId,
+ webTitle,
+ ajaxUrl,
+ switches,
+ isAdFreeUser,
+ section,
+ shouldHideReaderRevenue,
+ tags,
+ isPaidContent,
+ contentType,
+ sectionName,
+ isPreview,
+ idUrl,
+ isSensitive,
+ isDev,
+ onFirstPage,
+ keyEvents,
+ filterKeyEvents,
+ availableTopics,
+ selectedTopics,
+ keywordIds,
+ platform,
+ } = props;
const isInteractive = format.design === ArticleDesign.Interactive;
const palette = decidePalette(format);
@@ -176,13 +187,13 @@ export const ArticleBody = ({
shouldHideReaderRevenue={shouldHideReaderRevenue}
tags={tags}
isPaidContent={isPaidContent}
- contributionsServiceUrl={contributionsServiceUrl}
onFirstPage={onFirstPage}
keyEvents={keyEvents}
filterKeyEvents={filterKeyEvents}
availableTopics={availableTopics}
selectedTopics={selectedTopics}
keywordIds={keywordIds}
+ {...(platform === Platform.Web ? { platform, contributionsServiceUrl: props.contributionsServiceUrl } : { platform })}
/>
);
diff --git a/dotcom-rendering/src/web/components/LiveBlock.tsx b/dotcom-rendering/src/web/components/LiveBlock.tsx
index e30ea3c247a..6d01337290d 100644
--- a/dotcom-rendering/src/web/components/LiveBlock.tsx
+++ b/dotcom-rendering/src/web/components/LiveBlock.tsx
@@ -11,7 +11,7 @@ type Props = {
block: Block;
pageId: string;
webTitle: string;
- adTargeting: AdTargeting;
+ adTargeting?: AdTargeting;
host?: string;
ajaxUrl: string;
isAdFreeUser: boolean;
diff --git a/dotcom-rendering/src/web/lib/LiveBlogRenderer.tsx b/dotcom-rendering/src/web/lib/LiveBlogRenderer.tsx
index ab8ddc2d3ae..e3e389728e7 100644
--- a/dotcom-rendering/src/web/lib/LiveBlogRenderer.tsx
+++ b/dotcom-rendering/src/web/lib/LiveBlogRenderer.tsx
@@ -13,10 +13,10 @@ import {
TopicFilterBank,
} from '../components/TopicFilterBank.importable';
-type Props = {
+type CommonProps = {
format: ArticleFormat;
blocks: Block[];
- adTargeting: AdTargeting;
+ adTargeting?: AdTargeting;
pinnedPost?: Block;
host?: string;
pageId: string;
@@ -31,7 +31,6 @@ type Props = {
tags: TagType[];
isPaidContent: boolean;
keywordIds: string;
- contributionsServiceUrl: string;
onFirstPage?: boolean;
keyEvents?: Block[];
filterKeyEvents?: boolean;
@@ -39,34 +38,46 @@ type Props = {
selectedTopics?: Topic[];
};
-export const LiveBlogRenderer = ({
- format,
- blocks,
- pinnedPost,
- adTargeting,
- host,
- pageId,
- webTitle,
- ajaxUrl,
- switches,
- isAdFreeUser,
- isSensitive,
- isLiveUpdate,
- section,
- shouldHideReaderRevenue,
- tags,
- isPaidContent,
- keywordIds,
- contributionsServiceUrl,
- onFirstPage,
- keyEvents,
- filterKeyEvents = false,
- availableTopics,
- selectedTopics,
-}: Props) => {
+type AppsProps = {
+ platform: Platform.Apps;
+}
+
+type WebProps = {
+ platform: Platform.Web;
+ contributionsServiceUrl: string;
+}
+
+type Props = CommonProps & (AppsProps | WebProps);
+
+export const LiveBlogRenderer = (props: Props) => {
+ const {
+ format,
+ blocks,
+ pinnedPost,
+ adTargeting,
+ host,
+ pageId,
+ webTitle,
+ ajaxUrl,
+ switches,
+ isAdFreeUser,
+ isSensitive,
+ isLiveUpdate,
+ section,
+ shouldHideReaderRevenue,
+ tags,
+ isPaidContent,
+ keywordIds,
+ onFirstPage,
+ keyEvents,
+ filterKeyEvents = false,
+ availableTopics,
+ selectedTopics,
+ platform,
+ } = props;
const filtered =
(selectedTopics && selectedTopics.length > 0) || filterKeyEvents;
-
+
return (
<>
{pinnedPost && onFirstPage && !filtered && (
@@ -151,14 +162,14 @@ export const LiveBlogRenderer = ({
/>
);
})}
- {blocks.length > 4 && (
+ {blocks.length > 4 && platform === Platform.Web && (
diff --git a/dotcom-rendering/src/web/server/blocksToHtml.tsx b/dotcom-rendering/src/web/server/blocksToHtml.tsx
index 41893f4b7d4..ae407d17b78 100644
--- a/dotcom-rendering/src/web/server/blocksToHtml.tsx
+++ b/dotcom-rendering/src/web/server/blocksToHtml.tsx
@@ -58,6 +58,7 @@ export const blocksToHtml = ({
isPaidContent={false}
contributionsServiceUrl=""
keywordIds={keywordIds}
+ platform={Platform.Web}
/>,
);
From 3fd8b13893c82f29cf6eed8c00c6f3b33bd1001c Mon Sep 17 00:00:00 2001
From: Olly <9575458+OllysCoding@users.noreply.github.com>
Date: Wed, 14 Dec 2022 16:35:56 +0000
Subject: [PATCH 03/24] Get apps page to render
---
dotcom-rendering/src/app/layouts/StandardLayout.tsx | 5 ++---
dotcom-rendering/src/app/server/articleToHtml.tsx | 12 ++++++------
dotcom-rendering/src/app/server/pageTemplate.ts | 1 +
dotcom-rendering/src/types/platform.ts | 10 +++++-----
dotcom-rendering/src/web/browser/islands/init.ts | 4 ++++
dotcom-rendering/src/web/components/ArticleBody.tsx | 13 ++++++++++---
dotcom-rendering/src/web/layouts/CommentLayout.tsx | 2 ++
.../src/web/layouts/ImmersiveLayout.tsx | 2 ++
.../src/web/layouts/InteractiveLayout.tsx | 2 ++
dotcom-rendering/src/web/layouts/LiveLayout.tsx | 3 +++
dotcom-rendering/src/web/layouts/ShowcaseLayout.tsx | 2 ++
dotcom-rendering/src/web/layouts/StandardLayout.tsx | 2 ++
dotcom-rendering/src/web/lib/LiveBlogRenderer.tsx | 7 ++++---
dotcom-rendering/src/web/server/blocksToHtml.tsx | 3 ++-
14 files changed, 47 insertions(+), 21 deletions(-)
diff --git a/dotcom-rendering/src/app/layouts/StandardLayout.tsx b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
index 5f8a44e24f3..5a6b9366414 100644
--- a/dotcom-rendering/src/app/layouts/StandardLayout.tsx
+++ b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
@@ -10,6 +10,7 @@ import {
} from '@guardian/source-foundations';
import { StraightLines } from '@guardian/source-react-components-development-kitchen';
import type { FEArticleType } from '../../types/frontend';
+import { Platform } from '../../types/platform';
import { ArticleBody } from '../../web/components/ArticleBody';
import { ArticleContainer } from '../../web/components/ArticleContainer';
import { ArticleHeadline } from '../../web/components/ArticleHeadline';
@@ -489,6 +490,7 @@ export const StandardLayout = ({ CAPIArticle, format }: Props) => {
)}
{
isPaidContent={
!!CAPIArticle.config.isPaidContent
}
- contributionsServiceUrl={
- contributionsServiceUrl
- }
contentType={CAPIArticle.contentType}
sectionName={CAPIArticle.sectionName || ''}
isPreview={CAPIArticle.config.isPreview}
diff --git a/dotcom-rendering/src/app/server/articleToHtml.tsx b/dotcom-rendering/src/app/server/articleToHtml.tsx
index 69e2325d776..f7d9366f089 100644
--- a/dotcom-rendering/src/app/server/articleToHtml.tsx
+++ b/dotcom-rendering/src/app/server/articleToHtml.tsx
@@ -15,10 +15,10 @@ import {
// import { makeWindowGuardian } from '../../model/window-guardian';
import type { CAPIElement } from '../../types/content';
import type { FEArticleType } from '../../types/frontend';
-import { ArticlePage } from '../components/ArticlePage';
import { decideFormat } from '../../web/lib/decideFormat';
import { decideTheme } from '../../web/lib/decideTheme';
import { getHttp3Url } from '../../web/lib/getHttp3Url';
+import { ArticlePage } from '../components/ArticlePage';
import { pageTemplate } from './pageTemplate';
interface Props {
@@ -103,11 +103,11 @@ export const articleToHtml = ({ article }: Props): string => {
*/
const priorityScriptTags = generateScriptTags(
[
- ...getScriptArrayFromFile('bootCmp.js'),
- ...getScriptArrayFromFile('ophan.js'),
- process.env.COMMERCIAL_BUNDLE_URL ??
- article.config.commercialBundleUrl,
- ...getScriptArrayFromFile('sentryLoader.js'),
+ // ...getScriptArrayFromFile('bootCmp.js'),
+ // ...getScriptArrayFromFile('ophan.js'),
+ // process.env.COMMERCIAL_BUNDLE_URL ??
+ // article.config.commercialBundleUrl,
+ // ...getScriptArrayFromFile('sentryLoader.js'),
...getScriptArrayFromFile('dynamicImport.js'),
pageHasNonBootInteractiveElements &&
`${ASSET_ORIGIN}static/frontend/js/curl-with-js-and-domReady.js`,
diff --git a/dotcom-rendering/src/app/server/pageTemplate.ts b/dotcom-rendering/src/app/server/pageTemplate.ts
index 9e0d303da5f..13acaae7d33 100644
--- a/dotcom-rendering/src/app/server/pageTemplate.ts
+++ b/dotcom-rendering/src/app/server/pageTemplate.ts
@@ -169,6 +169,7 @@ https://workforus.theguardian.com/careers/product-engineering/
diff --git a/dotcom-rendering/src/types/platform.ts b/dotcom-rendering/src/types/platform.ts
index e52a113f950..cb2899d4e83 100644
--- a/dotcom-rendering/src/types/platform.ts
+++ b/dotcom-rendering/src/types/platform.ts
@@ -1,6 +1,6 @@
-enum Platform {
- Web,
- AMP,
- Apps,
- Editions,
+export enum Platform {
+ Web,
+ AMP,
+ Apps,
+ Editions,
}
diff --git a/dotcom-rendering/src/web/browser/islands/init.ts b/dotcom-rendering/src/web/browser/islands/init.ts
index 9baa2895471..1b27223857d 100644
--- a/dotcom-rendering/src/web/browser/islands/init.ts
+++ b/dotcom-rendering/src/web/browser/islands/init.ts
@@ -4,10 +4,14 @@ import { startup } from '../startup';
import { initHydration } from './initHydration';
const init = () => {
+ console.log('hello world2');
+
const elements = document.querySelectorAll('gu-island');
initHydration(elements);
return Promise.resolve();
};
+console.log('hello world');
+
startup('islands', null, init);
diff --git a/dotcom-rendering/src/web/components/ArticleBody.tsx b/dotcom-rendering/src/web/components/ArticleBody.tsx
index 187ac67693b..cfb726bfc81 100644
--- a/dotcom-rendering/src/web/components/ArticleBody.tsx
+++ b/dotcom-rendering/src/web/components/ArticleBody.tsx
@@ -5,6 +5,7 @@ import { between, body, headline, space } from '@guardian/source-foundations';
import { isRecipe } from '../../model/enhance-recipes';
import type { Switches } from '../../types/config';
import type { Palette } from '../../types/palette';
+import { Platform } from '../../types/platform';
import type { TagType } from '../../types/tag';
import { ArticleRenderer } from '../lib/ArticleRenderer';
import { decidePalette } from '../lib/decidePalette';
@@ -44,12 +45,12 @@ type CommonProps = {
type AppsProps = {
platform: Platform.Apps;
-}
+};
type WebProps = {
platform: Platform.Web;
contributionsServiceUrl: string;
-}
+};
type Props = CommonProps & (AppsProps | WebProps);
@@ -193,7 +194,13 @@ export const ArticleBody = (props: Props) => {
availableTopics={availableTopics}
selectedTopics={selectedTopics}
keywordIds={keywordIds}
- {...(platform === Platform.Web ? { platform, contributionsServiceUrl: props.contributionsServiceUrl } : { platform })}
+ {...(platform === Platform.Web
+ ? {
+ platform,
+ contributionsServiceUrl:
+ props.contributionsServiceUrl,
+ }
+ : { platform })}
/>
);
diff --git a/dotcom-rendering/src/web/layouts/CommentLayout.tsx b/dotcom-rendering/src/web/layouts/CommentLayout.tsx
index 19eaee32de8..388b24dbe0f 100644
--- a/dotcom-rendering/src/web/layouts/CommentLayout.tsx
+++ b/dotcom-rendering/src/web/layouts/CommentLayout.tsx
@@ -15,6 +15,7 @@ import { getSoleContributor } from '../../lib/byline';
import { parse } from '../../lib/slot-machine-flags';
import type { NavType } from '../../model/extract-nav';
import type { FEArticleType } from '../../types/frontend';
+import { Platform } from '../../types/platform';
import { AdSlot, MobileStickyContainer } from '../components/AdSlot';
import { ArticleBody } from '../components/ArticleBody';
import { ArticleContainer } from '../components/ArticleContainer';
@@ -575,6 +576,7 @@ export const CommentLayout = ({ CAPIArticle, NAV, format }: Props) => {
)}
{
} = props;
const filtered =
(selectedTopics && selectedTopics.length > 0) || filterKeyEvents;
-
+
return (
<>
{pinnedPost && onFirstPage && !filtered && (
diff --git a/dotcom-rendering/src/web/server/blocksToHtml.tsx b/dotcom-rendering/src/web/server/blocksToHtml.tsx
index ae407d17b78..8f42ab63177 100644
--- a/dotcom-rendering/src/web/server/blocksToHtml.tsx
+++ b/dotcom-rendering/src/web/server/blocksToHtml.tsx
@@ -1,5 +1,6 @@
import { renderToString } from 'react-dom/server';
import { buildAdTargeting } from '../../lib/ad-targeting';
+import { Platform } from '../../types/platform';
import { decideFormat } from '../lib/decideFormat';
import { LiveBlogRenderer } from '../lib/LiveBlogRenderer';
@@ -40,6 +41,7 @@ export const blocksToHtml = ({
const html = renderToString(
,
);
From bcf1f0a400eefab71aef4ad469b5aba818e32919 Mon Sep 17 00:00:00 2001
From: Olly <9575458+OllysCoding@users.noreply.github.com>
Date: Wed, 14 Dec 2022 17:29:55 +0000
Subject: [PATCH 04/24] feat: add a client side bundle for apps
---
.../scripts/webpack/webpack.config.browser.js | 71 ++++++++++++++-----
.../scripts/webpack/webpack.config.js | 11 ++-
dotcom-rendering/src/app/client/init.ts | 7 ++
.../src/app/server/articleToHtml.tsx | 30 +-------
dotcom-rendering/src/lib/assets.ts | 9 +++
5 files changed, 83 insertions(+), 45 deletions(-)
create mode 100644 dotcom-rendering/src/app/client/init.ts
diff --git a/dotcom-rendering/scripts/webpack/webpack.config.browser.js b/dotcom-rendering/scripts/webpack/webpack.config.browser.js
index e8637d144e9..6a5821db51a 100644
--- a/dotcom-rendering/scripts/webpack/webpack.config.browser.js
+++ b/dotcom-rendering/scripts/webpack/webpack.config.browser.js
@@ -1,3 +1,4 @@
+const webpack = require('webpack');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const GuStatsReportPlugin = require('./plugins/gu-stats-report-plugin');
@@ -13,7 +14,7 @@ const generateName = (bundle) => {
};
/**
- * @param {'legacy' | 'modern' | 'variant'} bundle
+ * @param {'legacy' | 'modern' | 'variant' | 'apps'} bundle
* @returns {string}
*/
const getLoaders = (bundle) => {
@@ -46,6 +47,32 @@ const getLoaders = (bundle) => {
},
},
];
+ case 'apps':
+ return [
+ {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ '@babel/preset-react',
+ [
+ '@babel/preset-env',
+ {
+ bugfixes: true,
+ targets: ['android >= 5', 'ios >= 12'],
+ },
+ ],
+ ],
+ compact: true,
+ },
+ },
+ {
+ loader: 'ts-loader',
+ options: {
+ configFile: 'tsconfig.build.json',
+ transpileOnly: true,
+ },
+ },
+ ];
case 'variant':
case 'modern':
return [
@@ -78,25 +105,28 @@ const getLoaders = (bundle) => {
};
/**
- * @param {{ bundle: 'legacy' | 'modern' | 'variant', sessionId: string }} options
+ * @param {{ bundle: 'legacy' | 'modern' | 'apps' | 'variant', sessionId: string }} options
* @returns {import('webpack').Configuration}
*/
module.exports = ({ bundle, sessionId }) => ({
- entry: {
- sentryLoader: './src/web/browser/sentryLoader/init.ts',
- bootCmp: './src/web/browser/bootCmp/init.ts',
- ga: './src/web/browser/ga/init.ts',
- ophan: './src/web/browser/ophan/init.ts',
- islands: './src/web/browser/islands/init.ts',
- dynamicImport: './src/web/browser/dynamicImport/init.ts',
- atomIframe: './src/web/browser/atomIframe/init.ts',
- embedIframe: './src/web/browser/embedIframe/init.ts',
- newsletterEmbedIframe:
- './src/web/browser/newsletterEmbedIframe/init.ts',
- relativeTime: './src/web/browser/relativeTime/init.ts',
- initDiscussion: './src/web/browser/initDiscussion/init.ts',
- debug: './src/web/browser/debug/init.ts',
- },
+ entry:
+ bundle === 'apps'
+ ? './src/app/client/init.ts'
+ : {
+ sentryLoader: './src/web/browser/sentryLoader/init.ts',
+ bootCmp: './src/web/browser/bootCmp/init.ts',
+ ga: './src/web/browser/ga/init.ts',
+ ophan: './src/web/browser/ophan/init.ts',
+ islands: './src/web/browser/islands/init.ts',
+ dynamicImport: './src/web/browser/dynamicImport/init.ts',
+ atomIframe: './src/web/browser/atomIframe/init.ts',
+ embedIframe: './src/web/browser/embedIframe/init.ts',
+ newsletterEmbedIframe:
+ './src/web/browser/newsletterEmbedIframe/init.ts',
+ relativeTime: './src/web/browser/relativeTime/init.ts',
+ initDiscussion: './src/web/browser/initDiscussion/init.ts',
+ debug: './src/web/browser/debug/init.ts',
+ },
output: {
filename: (data) => {
// We don't want to hash the debug script so it can be used in bookmarklets
@@ -110,6 +140,13 @@ module.exports = ({ bundle, sessionId }) => ({
new WebpackManifestPlugin({
fileName: `manifest.${bundle}.json`,
}),
+ ...(bundle === 'apps'
+ ? [
+ new webpack.optimize.LimitChunkCountPlugin({
+ maxChunks: 1,
+ }),
+ ]
+ : []),
...(DEV
? [
new GuStatsReportPlugin({
diff --git a/dotcom-rendering/scripts/webpack/webpack.config.js b/dotcom-rendering/scripts/webpack/webpack.config.js
index 782369b91a7..d901e288b81 100644
--- a/dotcom-rendering/scripts/webpack/webpack.config.js
+++ b/dotcom-rendering/scripts/webpack/webpack.config.js
@@ -19,7 +19,7 @@ const sessionId = uuidv4();
let builds = 0;
/**
- * @param {{ platform: 'server' | 'browser.legacy' | 'browser.modern' | 'browser.variant'}} options
+ * @param {{ platform: 'server' | 'browser.legacy' | 'browser.modern' | 'browser.apps' | 'browser.variant'}} options
* @returns {import('webpack').Configuration}
*/
const commonConfigs = ({ platform }) => ({
@@ -131,6 +131,15 @@ module.exports = [
sessionId,
}),
),
+ merge(
+ commonConfigs({
+ platform: 'browser.apps',
+ }),
+ require(`./webpack.config.browser`)({
+ bundle: 'apps',
+ sessionId,
+ }),
+ ),
...(BUILD_VARIANT
? [
merge(
diff --git a/dotcom-rendering/src/app/client/init.ts b/dotcom-rendering/src/app/client/init.ts
new file mode 100644
index 00000000000..54ecf18b598
--- /dev/null
+++ b/dotcom-rendering/src/app/client/init.ts
@@ -0,0 +1,7 @@
+import '../../web/browser/dynamicImport/init.ts';
+import '../../web/browser/islands/init.ts';
+import '../../web/browser/atomIframe/init.ts';
+import '../../web/browser/embedIframe/init.ts';
+import '../../web/browser/newsletterEmbedIframe/init.ts';
+import '../../web/browser/relativeTime/init.ts';
+// import '../../web/browser/initDiscussion/init.ts';
diff --git a/dotcom-rendering/src/app/server/articleToHtml.tsx b/dotcom-rendering/src/app/server/articleToHtml.tsx
index f7d9366f089..7f22a53e338 100644
--- a/dotcom-rendering/src/app/server/articleToHtml.tsx
+++ b/dotcom-rendering/src/app/server/articleToHtml.tsx
@@ -10,6 +10,7 @@ import {
import {
ASSET_ORIGIN,
generateScriptTags,
+ getAppScript,
getScriptsFromManifest,
} from '../../lib/assets';
// import { makeWindowGuardian } from '../../model/window-guardian';
@@ -102,37 +103,12 @@ export const articleToHtml = ({ article }: Props): string => {
* Scripts will be executed in the order they appear in this array
*/
const priorityScriptTags = generateScriptTags(
- [
- // ...getScriptArrayFromFile('bootCmp.js'),
- // ...getScriptArrayFromFile('ophan.js'),
- // process.env.COMMERCIAL_BUNDLE_URL ??
- // article.config.commercialBundleUrl,
- // ...getScriptArrayFromFile('sentryLoader.js'),
- ...getScriptArrayFromFile('dynamicImport.js'),
- pageHasNonBootInteractiveElements &&
- `${ASSET_ORIGIN}static/frontend/js/curl-with-js-and-domReady.js`,
- ...getScriptArrayFromFile('islands.js'),
- ].map((script) =>
+ [getAppScript()].map((script) =>
offerHttp3 && script ? getHttp3Url(script) : script,
),
);
- /**
- * Low priority scripts. These scripts will be requested
- * asynchronously after the main HTML has been parsed. Execution
- * order is not guaranteed. It is even possible that these execute
- * *before* the high priority scripts, although this is very
- * unlikely.
- */
- const lowPriorityScriptTags = generateScriptTags(
- [
- ...getScriptArrayFromFile('atomIframe.js'),
- ...getScriptArrayFromFile('embedIframe.js'),
- ...getScriptArrayFromFile('newsletterEmbedIframe.js'),
- ...getScriptArrayFromFile('relativeTime.js'),
- ...getScriptArrayFromFile('initDiscussion.js'),
- ].map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
- );
+ const lowPriorityScriptTags: string[] = [];
/**
* We escape windowGuardian here to prevent errors when the data
diff --git a/dotcom-rendering/src/lib/assets.ts b/dotcom-rendering/src/lib/assets.ts
index 3118a0d2e5b..3f84740e7d6 100644
--- a/dotcom-rendering/src/lib/assets.ts
+++ b/dotcom-rendering/src/lib/assets.ts
@@ -90,6 +90,15 @@ const getScripts = (
});
};
+export const getAppScript = (): string => {
+ if (isDev) {
+ return `${ASSET_ORIGIN}assets/main.apps.js`;
+ }
+
+ const manifest = getManifest('./manifest.apps.json');
+ return `${ASSET_ORIGIN}assets/${manifest['main.js']}`;
+};
+
/**
* A curried function that takes an array of manifests.
*
From c0009a9f6b50c96521e1176eb88852a7a3f15963 Mon Sep 17 00:00:00 2001
From: Olly <9575458+OllysCoding@users.noreply.github.com>
Date: Thu, 15 Dec 2022 12:46:35 +0000
Subject: [PATCH 05/24] Add initial bridget support to DCR
---
dotcom-rendering/package.json | 2 +
.../src/app/layouts/StandardLayout.tsx | 2 +
dotcom-rendering/src/app/native/nativeApi.ts | 72 +++++++
.../src/app/native/thrift/nativeConnection.ts | 159 ++++++++++++++++
.../src/app/native/thrift/protocols.ts | 176 ++++++++++++++++++
dotcom-rendering/src/types/palette.ts | 1 +
.../src/web/components/ArticleMeta.tsx | 9 +
.../src/web/components/Follow.importable.tsx | 144 ++++++++++++++
dotcom-rendering/src/web/lib/decidePalette.ts | 31 +++
yarn.lock | 97 ++++++++--
10 files changed, 681 insertions(+), 12 deletions(-)
create mode 100644 dotcom-rendering/src/app/native/nativeApi.ts
create mode 100644 dotcom-rendering/src/app/native/thrift/nativeConnection.ts
create mode 100644 dotcom-rendering/src/app/native/thrift/protocols.ts
create mode 100644 dotcom-rendering/src/web/components/Follow.importable.tsx
diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json
index 83f6e3332ea..4038153ec1b 100644
--- a/dotcom-rendering/package.json
+++ b/dotcom-rendering/package.json
@@ -62,6 +62,7 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@braze/web-sdk-core": "3.5.1",
+ "@creditkarma/thrift-server-core": "^1.0.4",
"@cypress/skip-test": "^2.6.0",
"@emotion/babel-plugin": "^11.3.0",
"@emotion/cache": "^11.4.0",
@@ -71,6 +72,7 @@
"@guardian/ab-react": "^2.0.1",
"@guardian/atoms-rendering": "^24.0.0",
"@guardian/braze-components": "^8.1.3",
+ "@guardian/bridget": "^2.0.0",
"@guardian/browserslist-config": "^2.0.3",
"@guardian/commercial-core": "^5.0.0",
"@guardian/consent-management-platform": "11.0.0",
diff --git a/dotcom-rendering/src/app/layouts/StandardLayout.tsx b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
index 5a6b9366414..86a56eced68 100644
--- a/dotcom-rendering/src/app/layouts/StandardLayout.tsx
+++ b/dotcom-rendering/src/app/layouts/StandardLayout.tsx
@@ -20,6 +20,7 @@ import { Border } from '../../web/components/Border';
import { Carousel } from '../../web/components/Carousel.importable';
import { DecideLines } from '../../web/components/DecideLines';
import { DecideOnwards } from '../../web/components/DecideOnwards';
+import { Follow } from '../../web/components/Follow.importable';
import { GetMatchNav } from '../../web/components/GetMatchNav.importable';
import { GetMatchStats } from '../../web/components/GetMatchStats.importable';
import { GetMatchTabs } from '../../web/components/GetMatchTabs.importable';
@@ -454,6 +455,7 @@ export const StandardLayout = ({ CAPIArticle, format }: Props) => {
= createAppClient<
+ Environment.Client
+>(Environment.Client, 'buffered', 'compact');
+const commercialClient: Commercial.Client = createAppClient<
+ Commercial.Client
+>(Commercial.Client, 'buffered', 'compact');
+const acquisitionsClient: Acquisitions.Client = createAppClient<
+ Acquisitions.Client
+>(Acquisitions.Client, 'buffered', 'compact');
+const notificationsClient: Notifications.Client = createAppClient<
+ Notifications.Client
+>(Notifications.Client, 'buffered', 'compact');
+const userClient: User.Client = createAppClient>(
+ User.Client,
+ 'buffered',
+ 'compact',
+);
+const galleryClient: Gallery.Client = createAppClient<
+ Gallery.Client
+>(Gallery.Client, 'buffered', 'compact');
+const videoClient: Video.Client = createAppClient>(
+ Video.Client,
+ 'buffered',
+ 'compact',
+);
+const metricsClient: Metrics.Client = createAppClient<
+ Metrics.Client
+>(Metrics.Client, 'buffered', 'compact');
+const discussionClient: Discussion.Client = createAppClient<
+ Discussion.Client
+>(Discussion.Client, 'buffered', 'compact');
+
+const analyticsClient: Analytics.Client = createAppClient<
+ Analytics.Client
+>(Analytics.Client, 'buffered', 'compact');
+
+const navigationClient: Navigation.Client = createAppClient<
+ Navigation.Client
+>(Navigation.Client, 'buffered', 'compact');
+
+const newslettersClient: Newsletters.Client = createAppClient<
+ Newsletters.Client
+>(Newsletters.Client, 'buffered', 'compact');
+
+export {
+ environmentClient,
+ commercialClient,
+ acquisitionsClient,
+ notificationsClient,
+ userClient,
+ galleryClient,
+ videoClient,
+ metricsClient,
+ discussionClient,
+ analyticsClient,
+ navigationClient,
+ newslettersClient,
+};
diff --git a/dotcom-rendering/src/app/native/thrift/nativeConnection.ts b/dotcom-rendering/src/app/native/thrift/nativeConnection.ts
new file mode 100644
index 00000000000..a3b9b12f709
--- /dev/null
+++ b/dotcom-rendering/src/app/native/thrift/nativeConnection.ts
@@ -0,0 +1,159 @@
+import type {
+ IClientConstructor,
+ IProtocolConstructor,
+ ITransportConstructor,
+ ProtocolType,
+ ThriftClient,
+ TransportType,
+ TTransport,
+} from '@creditkarma/thrift-server-core';
+import {
+ getProtocol,
+ getTransport,
+ TApplicationException,
+ TApplicationExceptionType,
+ ThriftConnection,
+} from '@creditkarma/thrift-server-core';
+import * as uuid from 'uuid';
+import { TMultiplexedProtocol } from './protocols';
+
+declare global {
+ interface Window {
+ nativeConnections?: Record;
+ android?: {
+ postMessage: (data: string, connectionId: string) => void;
+ };
+ webkit?: {
+ messageHandlers: {
+ iOSWebViewMessage: {
+ postMessage: (nativeMessage: NativeMessage) => void;
+ };
+ };
+ };
+ }
+}
+
+export interface NativeMessage {
+ data: string;
+ connectionId: string;
+}
+
+interface PromiseResponse {
+ resolve: (response: Buffer) => void;
+ reject: (error: Error) => void;
+ timeoutId: NodeJS.Timeout;
+}
+
+const ACTION_TIMEOUT_MS = 30000;
+
+function sendNativeMessage(nativeMessage: NativeMessage): void {
+ if (window.android) {
+ window.android.postMessage(
+ nativeMessage.data,
+ nativeMessage.connectionId,
+ );
+ } else if (window.webkit) {
+ window.webkit.messageHandlers.iOSWebViewMessage.postMessage(
+ nativeMessage,
+ );
+ } else {
+ console.warn('No native APIs available');
+ }
+}
+
+export class NativeConnection extends ThriftConnection {
+ connectionId = uuid.v4();
+ promises: PromiseResponse[] = [];
+ outBuffer: NativeMessage[] = [];
+
+ constructor(
+ Transport: ITransportConstructor,
+ Protocol: IProtocolConstructor,
+ ) {
+ super(Transport, Protocol);
+ if (typeof window !== 'undefined') {
+ window.nativeConnections = window.nativeConnections ?? {};
+ window.nativeConnections[this.connectionId] = this;
+ }
+ }
+
+ reset(oldConnectionId: string): void {
+ if (oldConnectionId === this.connectionId && window.nativeConnections) {
+ console.warn('Reseting connection ' + oldConnectionId);
+ delete window.nativeConnections[this.connectionId];
+ this.promises.forEach((promise) => {
+ promise.reject(
+ new TApplicationException(
+ TApplicationExceptionType.UNKNOWN,
+ 'Timeout error',
+ ),
+ );
+ });
+ this.promises = [];
+ this.connectionId = uuid.v4();
+ window.nativeConnections[this.connectionId] = this;
+ }
+ }
+
+ receive(message: NativeMessage): void {
+ const resolver = this.promises.shift();
+ if (resolver) {
+ clearTimeout(resolver.timeoutId);
+ const data = Buffer.from(message.data, 'base64');
+ resolver.resolve(data);
+ }
+ this.sendNextMessage();
+ }
+
+ private sendNextMessage(): void {
+ const message = this.outBuffer.shift();
+ if (message) {
+ console.log('Sending next message');
+ sendNativeMessage(message);
+ }
+ }
+
+ send(dataToSend: Buffer, context?: void | undefined): Promise {
+ const id = this.connectionId;
+ // eslint-disable-next-line @typescript-eslint/no-this-alias -- Reassign this
+ const connection = this;
+ return new Promise(function (res, rej): void {
+ connection.promises.push({
+ resolve: res,
+ reject: rej,
+ timeoutId: setTimeout(function () {
+ connection.reset(id);
+ }, ACTION_TIMEOUT_MS),
+ });
+ const message: NativeMessage = {
+ data: dataToSend.toString('base64'),
+ connectionId: id,
+ };
+ if (connection.promises.length === 1) {
+ console.log('Sending message immediately');
+ sendNativeMessage(message);
+ } else {
+ console.log('Queing message because others in flight');
+ connection.outBuffer.push(message);
+ }
+ });
+ }
+}
+
+export function createAppClient>(
+ ServiceClient: IClientConstructor,
+ transport: TransportType = 'buffered',
+ protocol: ProtocolType = 'compact',
+): TClient {
+ class NamedMultiplexedProtocol extends TMultiplexedProtocol {
+ constructor(transport: TTransport) {
+ const Protocol = getProtocol(protocol);
+ super(new Protocol(transport), ServiceClient.serviceName ?? '');
+ }
+ }
+ const connection = new NativeConnection(
+ getTransport(transport),
+ NamedMultiplexedProtocol,
+ );
+ return new ServiceClient(connection);
+}
diff --git a/dotcom-rendering/src/app/native/thrift/protocols.ts b/dotcom-rendering/src/app/native/thrift/protocols.ts
new file mode 100644
index 00000000000..8878b7f4458
--- /dev/null
+++ b/dotcom-rendering/src/app/native/thrift/protocols.ts
@@ -0,0 +1,176 @@
+import type {
+ Int64,
+ IThriftField,
+ IThriftList,
+ IThriftMap,
+ IThriftMessage,
+ IThriftSet,
+ IThriftStruct,
+ TTransport,
+ TType,
+} from '@creditkarma/thrift-server-core';
+import { MessageType, TProtocol } from '@creditkarma/thrift-server-core';
+
+export abstract class TProtocolDecorator extends TProtocol {
+ private concreteProtocol: TProtocol;
+
+ constructor(protocol: TProtocol) {
+ super(protocol.getTransport());
+ this.concreteProtocol = protocol;
+ }
+
+ getTransport(): TTransport {
+ return this.concreteProtocol.getTransport();
+ }
+ flush(): Buffer {
+ return this.concreteProtocol.flush();
+ }
+ writeMessageBegin(name: string, type: MessageType, seqid: number): void {
+ return this.concreteProtocol.writeMessageBegin(name, type, seqid);
+ }
+ writeMessageEnd(): void {
+ return this.concreteProtocol.writeMessageEnd();
+ }
+ writeStructBegin(name: string): void {
+ return this.concreteProtocol.writeStructBegin(name);
+ }
+ writeStructEnd(): void {
+ return this.concreteProtocol.writeStructEnd();
+ }
+ writeFieldBegin(name: string, type: TType, id: number): void {
+ return this.concreteProtocol.writeFieldBegin(name, type, id);
+ }
+ writeFieldEnd(): void {
+ return this.concreteProtocol.writeFieldEnd();
+ }
+ writeFieldStop(): void {
+ return this.concreteProtocol.writeFieldStop();
+ }
+ writeMapBegin(keyType: TType, valueType: TType, size: number): void {
+ return this.concreteProtocol.writeMapBegin(keyType, valueType, size);
+ }
+ writeMapEnd(): void {
+ return this.concreteProtocol.writeMapEnd();
+ }
+ writeListBegin(elementType: TType, size: number): void {
+ return this.concreteProtocol.writeListBegin(elementType, size);
+ }
+ writeListEnd(): void {
+ return this.concreteProtocol.writeListEnd();
+ }
+ writeSetBegin(elementType: TType, size: number): void {
+ return this.concreteProtocol.writeSetBegin(elementType, size);
+ }
+ writeSetEnd(): void {
+ return this.concreteProtocol.writeSetEnd();
+ }
+ writeBool(bool: boolean): void {
+ return this.concreteProtocol.writeBool(bool);
+ }
+ writeByte(b: number): void {
+ return this.concreteProtocol.writeByte(b);
+ }
+ writeI16(i16: number): void {
+ return this.concreteProtocol.writeI16(i16);
+ }
+ writeI32(i32: number): void {
+ return this.concreteProtocol.writeI32(i32);
+ }
+ writeI64(i64: number | Int64): void {
+ return this.concreteProtocol.writeI64(i64);
+ }
+ writeDouble(dbl: number): void {
+ return this.concreteProtocol.writeDouble(dbl);
+ }
+ writeString(arg: string): void {
+ return this.concreteProtocol.writeString(arg);
+ }
+ writeBinary(arg: string | Buffer): void {
+ return this.concreteProtocol.writeBinary(arg);
+ }
+ readMessageBegin(): IThriftMessage {
+ return this.concreteProtocol.readMessageBegin();
+ }
+ readMessageEnd(): void {
+ return this.concreteProtocol.readMessageEnd();
+ }
+ readStructBegin(): IThriftStruct {
+ return this.concreteProtocol.readStructBegin();
+ }
+ readStructEnd(): void {
+ return this.concreteProtocol.readStructEnd();
+ }
+ readFieldBegin(): IThriftField {
+ return this.concreteProtocol.readFieldBegin();
+ }
+ readFieldEnd(): void {
+ return this.concreteProtocol.readFieldEnd();
+ }
+ readMapBegin(): IThriftMap {
+ return this.concreteProtocol.readMapBegin();
+ }
+ readMapEnd(): void {
+ return this.concreteProtocol.readMapEnd();
+ }
+ readListBegin(): IThriftList {
+ return this.concreteProtocol.readListBegin();
+ }
+ readListEnd(): void {
+ return this.concreteProtocol.readListEnd();
+ }
+ readSetBegin(): IThriftSet {
+ return this.concreteProtocol.readSetBegin();
+ }
+ readSetEnd(): void {
+ return this.concreteProtocol.readSetEnd();
+ }
+ readBool(): boolean {
+ return this.concreteProtocol.readBool();
+ }
+ readByte(): number {
+ return this.concreteProtocol.readByte();
+ }
+ readI16(): number {
+ return this.concreteProtocol.readI16();
+ }
+ readI32(): number {
+ return this.concreteProtocol.readI32();
+ }
+ readI64(): Int64 {
+ return this.concreteProtocol.readI64();
+ }
+ readDouble(): number {
+ return this.concreteProtocol.readDouble();
+ }
+ readBinary(): Buffer {
+ return this.concreteProtocol.readBinary();
+ }
+ readString(): string {
+ return this.concreteProtocol.readString();
+ }
+ skip(type: TType): void {
+ return this.concreteProtocol.skip(type);
+ }
+}
+
+export class TMultiplexedProtocol extends TProtocolDecorator {
+ static readonly separator = ':';
+ readonly serviceName: string;
+
+ constructor(protocol: TProtocol, serviceName: string) {
+ super(protocol);
+ this.serviceName = serviceName;
+ }
+
+ writeMessageBegin(name: string, type: MessageType, seqid: number): void {
+ if (type === MessageType.CALL || type === MessageType.ONEWAY) {
+ super.writeMessageBegin(
+ this.serviceName + TMultiplexedProtocol.separator + name,
+ type,
+ seqid,
+ );
+ } else {
+ super.writeMessageBegin(name, type, seqid);
+ }
+ }
+}
diff --git a/dotcom-rendering/src/types/palette.ts b/dotcom-rendering/src/types/palette.ts
index 7371b9ce98a..0fa70a25b27 100644
--- a/dotcom-rendering/src/types/palette.ts
+++ b/dotcom-rendering/src/types/palette.ts
@@ -8,6 +8,7 @@ export type Palette = {
sectionTitle: Colour;
seriesTitleWhenMatch: Colour;
byline: Colour;
+ follow: Colour;
twitterHandle: Colour;
twitterHandleBelowDesktop: Colour;
caption: Colour;
diff --git a/dotcom-rendering/src/web/components/ArticleMeta.tsx b/dotcom-rendering/src/web/components/ArticleMeta.tsx
index 0cb589fbcc7..dfebb499a8b 100644
--- a/dotcom-rendering/src/web/components/ArticleMeta.tsx
+++ b/dotcom-rendering/src/web/components/ArticleMeta.tsx
@@ -12,6 +12,7 @@ import { StraightLines } from '@guardian/source-react-components-development-kit
import { getSoleContributor } from '../../lib/byline';
import type { Branding as BrandingType } from '../../types/branding';
import type { Palette } from '../../types/palette';
+import { Platform } from '../../types/platform';
import type { TagType } from '../../types/tag';
import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling';
import { decidePalette } from '../lib/decidePalette';
@@ -21,11 +22,13 @@ import { CommentCount } from './CommentCount.importable';
import { Contributor } from './Contributor';
import { Counts } from './Counts';
import { Dateline } from './Dateline';
+import { Follow } from './Follow.importable';
import { Island } from './Island';
import { ShareCount } from './ShareCount.importable';
import { ShareIcons } from './ShareIcons';
type Props = {
+ platform: Platform;
format: ArticleFormat;
pageId: string;
webTitle: string;
@@ -297,6 +300,7 @@ const metaNumbersExtrasLiveBlog = css`
`;
export const ArticleMeta = ({
+ platform,
branding,
format,
pageId,
@@ -379,6 +383,11 @@ export const ArticleMeta = ({
secondaryDateline={secondaryDateline}
format={format}
/>
+ {platform === Platform.Apps && (
+
+
+
+ )}
>
diff --git a/dotcom-rendering/src/web/components/Follow.importable.tsx b/dotcom-rendering/src/web/components/Follow.importable.tsx
new file mode 100644
index 00000000000..68a3bae2034
--- /dev/null
+++ b/dotcom-rendering/src/web/components/Follow.importable.tsx
@@ -0,0 +1,144 @@
+import { css } from '@emotion/react';
+import { Topic } from '@guardian/bridget/Topic';
+import { ArticleSpecial } from '@guardian/libs';
+import { space, textSans } from '@guardian/source-foundations';
+import { useEffect, useState } from 'react';
+import { notificationsClient } from '../../app/native/nativeApi';
+import type { Palette } from '../../types/palette';
+import type { TagType } from '../../types/tag';
+import { decidePalette } from '../lib/decidePalette';
+
+type Props = {
+ tags: TagType[];
+ format: ArticleFormat;
+};
+
+const getContributorTags = (tags: TagType[]): TagType[] =>
+ tags.filter((tag) => tag.type === 'Contributor');
+
+const FollowIcon = ({ isFollowing }: { isFollowing: boolean }) => {
+ const check =
+ 'M16.171 10.64L15.411 11.34L16.206 12.09L16.947 12.806L20 9.924L19.259 9.208L16.947 11.391L16.171 10.641L16.171 10.64Z';
+ const plus =
+ 'M17.667 13.5h-1v-2.167H14.5v-1h2.167V8.167h1v2.166h2.166v1h-2.166V13.5z';
+
+ return (
+
+ );
+};
+
+const buttonStyles = (palette: Palette) => css`
+ ${textSans.small()}
+ color: ${palette.text.follow};
+ display: block;
+ padding: 0;
+ border: none;
+ background: none;
+ margin-left: 0;
+ margin-top: ${space[1]}px;
+ min-height: ${space[6]}px;
+
+ svg {
+ width: ${space[6]}px;
+ height: ${space[6]}px;
+ fill: currentColor;
+ }
+
+ ${
+ /*darkModeCss`
+ color: ${text.followDark(format)};
+`*/ ''
+ }
+`;
+
+const spanStyles = css`
+ display: flex;
+ align-items: center;
+ column-gap: 0.2em;
+`;
+
+export const Follow = ({ tags, format }: Props) => {
+ const contributors = getContributorTags(tags);
+ if (contributors.length !== 1 || format.theme == ArticleSpecial.Labs)
+ return null;
+
+ const contributor = contributors[0];
+ if (!contributor.id.startsWith('profile/')) return null;
+
+ return ;
+};
+
+export const FollowButton = ({
+ contributor,
+ format,
+}: {
+ contributor: TagType;
+ format: ArticleFormat;
+}) => {
+ const palette = decidePalette(format);
+
+ const [isFollowing, setIsFollowing] = useState(false);
+
+ useEffect(() => {
+ const topic = new Topic({
+ id: contributor.id,
+ displayName: contributor.title,
+ type: 'tag-contributor',
+ });
+
+ notificationsClient
+ .isFollowing(topic)
+ .then((value) => {
+ setIsFollowing(value);
+ })
+ .catch(() => {});
+ }, [contributor]);
+
+ const onFollowingClick = () => {
+ const topic = new Topic({
+ id: contributor.id,
+ displayName: contributor.title,
+ type: 'tag-contributor',
+ });
+
+ if (isFollowing) {
+ notificationsClient
+ .unfollow(topic)
+ .then(() => setIsFollowing(false))
+ .catch(() => {});
+ } else {
+ notificationsClient
+ .follow(topic)
+ .then(() => setIsFollowing(true))
+ .catch(() => {});
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/dotcom-rendering/src/web/lib/decidePalette.ts b/dotcom-rendering/src/web/lib/decidePalette.ts
index 4a37f9e6dba..2b20f9924fc 100644
--- a/dotcom-rendering/src/web/lib/decidePalette.ts
+++ b/dotcom-rendering/src/web/lib/decidePalette.ts
@@ -216,6 +216,36 @@ const textByline = (format: ArticleFormat): string => {
}
};
+const textFollow = (format: ArticleFormat): string => {
+ if (format.design === ArticleDesign.Gallery) {
+ return neutral[86];
+ }
+
+ switch (format.theme) {
+ case ArticlePillar.News:
+ switch (format.design) {
+ case ArticleDesign.Analysis:
+ return news[300];
+ default:
+ return news[400];
+ }
+ case ArticlePillar.Lifestyle:
+ return lifestyle[300];
+ case ArticlePillar.Sport:
+ return sport[300];
+ case ArticlePillar.Culture:
+ return culture[300];
+ case ArticlePillar.Opinion:
+ return opinion[200];
+ case ArticleSpecial.Labs:
+ return labs[300];
+ case ArticleSpecial.SpecialReport:
+ return specialReport[300];
+ case ArticleSpecial.SpecialReportAlt:
+ return news[400];
+ }
+};
+
const textHeadlineByline = (format: ArticleFormat): string => {
if (format.design === ArticleDesign.Analysis) {
switch (format.theme) {
@@ -1967,6 +1997,7 @@ export const decidePalette = (
seriesTitleWhenMatch: textSeriesTitleWhenMatch(format),
sectionTitle: textSectionTitle(format),
byline: textByline(format),
+ follow: textFollow(format),
twitterHandle: textTwitterHandle(format),
twitterHandleBelowDesktop: textTwitterHandleBelowDesktop(format),
caption: textCaption(format),
diff --git a/yarn.lock b/yarn.lock
index 93b254e8be9..feb90695c03 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2683,6 +2683,14 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
+"@creditkarma/thrift-server-core@^1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@creditkarma/thrift-server-core/-/thrift-server-core-1.0.4.tgz#ee66a9bf77add286b762fa724da68d3aec2188fd"
+ integrity sha512-Jook5uFJqPeM/D0taSdKHeoerZB6HboSDMqBDWhVDJVSKJGWPSMch4GNALRqr8nCekLKMYkdCgj4FAVetnxpGA==
+ dependencies:
+ "@types/lodash" "^4.14.136"
+ lodash "^4.17.15"
+
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@@ -2963,6 +2971,11 @@
resolved "https://registry.yarnpkg.com/@guardian/braze-components/-/braze-components-8.1.3.tgz#47adc4892716904a62afa62da66f31eae059d56b"
integrity sha512-Tol4O7A3ErmzgCd9YtkajJUfCps/1RCNXhtaUBwgT8u0OUPRmfk4jjD7cS3nok5gDTEUVemKwT2yysx1CxfWaw==
+"@guardian/bridget@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@guardian/bridget/-/bridget-2.0.0.tgz#9be79e1f8c0c4a9ee3829390b9bcb828d78cb4d7"
+ integrity sha512-bCNIQuPe74AwR0eZWHgN/at3vumPOB0NxozciTujGUblhun23f+Fagtkkf1zyIqQ2VjaYmSVy5zVug+E5v9auw==
+
"@guardian/browserslist-config@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@guardian/browserslist-config/-/browserslist-config-2.0.3.tgz#1c8b832ab564b257f8146ca1b3183b7683026ec9"
@@ -5387,6 +5400,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
+"@types/lodash@^4.14.136":
+ version "4.14.191"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
+ integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+
"@types/lodash@^4.14.167":
version "4.14.189"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2"
@@ -5611,7 +5629,7 @@
dependencies:
"@types/express" "*"
-"@types/serve-static@*":
+"@types/serve-static@*", "@types/serve-static@^1.13.9":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==
@@ -5636,6 +5654,11 @@
dependencies:
"@types/node" "*"
+"@types/source-list-map@*":
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
+ integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -5646,7 +5669,7 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
-"@types/tapable@^1.0.5":
+"@types/tapable@^1", "@types/tapable@^1.0.5":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
@@ -5712,14 +5735,26 @@
"@types/node" "*"
webpack "^5"
-"@types/webpack@^4.41.26", "@types/webpack@^4.41.8", "@types/webpack@^5.28.0":
- version "5.28.0"
- resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03"
- integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==
+"@types/webpack-sources@*":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b"
+ integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==
dependencies:
"@types/node" "*"
- tapable "^2.2.0"
- webpack "^5"
+ "@types/source-list-map" "*"
+ source-map "^0.7.3"
+
+"@types/webpack@^4.41.26", "@types/webpack@^4.41.8":
+ version "4.41.33"
+ resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc"
+ integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==
+ dependencies:
+ "@types/node" "*"
+ "@types/tapable" "^1"
+ "@types/uglify-js" "*"
+ "@types/webpack-sources" "*"
+ anymatch "^3.0.0"
+ source-map "^0.6.0"
"@types/ws@^8.2.2":
version "8.2.2"
@@ -6809,6 +6844,14 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
+anymatch@^3.0.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@@ -20465,10 +20508,40 @@ type-detect@4.0.8:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
-type-fest@2.12.2, type-fest@^0.18.0, type-fest@^0.20.2, type-fest@^0.21.3, type-fest@^0.6.0, type-fest@^0.8.1, type-fest@^1.4.0, type-fest@^2.8.0:
- version "2.19.0"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
- integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+type-fest@2.12.2:
+ version "2.12.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.2.tgz#80a53614e6b9b475eb9077472fb7498dc7aa51d0"
+ integrity sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==
+
+type-fest@^0.18.0:
+ version "0.18.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+ integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
+
+type-fest@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+ integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type-fest@^0.21.3:
+ version "0.21.3"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+ integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+ integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+ integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+type-fest@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
+ integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
type-is@~1.6.18:
version "1.6.18"
From 4aad7b606c4302aa46123907b6e27de08d0708cd Mon Sep 17 00:00:00 2001
From: Olly <9575458+OllysCoding@users.noreply.github.com>
Date: Thu, 15 Dec 2022 14:55:14 +0000
Subject: [PATCH 06/24] add buffer to apps build
---
dotcom-rendering/package.json | 1 +
.../scripts/webpack/webpack.config.browser.js | 11 +++
.../src/web/components/Follow.importable.tsx | 8 +-
yarn.lock | 89 +++++--------------
4 files changed, 37 insertions(+), 72 deletions(-)
diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json
index 4038153ec1b..9bd6322fe34 100644
--- a/dotcom-rendering/package.json
+++ b/dotcom-rendering/package.json
@@ -150,6 +150,7 @@
"babel-plugin-polyfill-corejs3": "^0.6.0",
"babel-plugin-px-to-rem": "https://github.com/guardian/babel-plugin-px-to-rem#v0.1.0",
"babel-plugin-transform-runtime": "^6.23.0",
+ "buffer": "^6.0.3",
"bundlesize": "^0.18.1",
"chalk": "^4.1.0",
"compression": "^1.7.3",
diff --git a/dotcom-rendering/scripts/webpack/webpack.config.browser.js b/dotcom-rendering/scripts/webpack/webpack.config.browser.js
index 6a5821db51a..2c8874ab651 100644
--- a/dotcom-rendering/scripts/webpack/webpack.config.browser.js
+++ b/dotcom-rendering/scripts/webpack/webpack.config.browser.js
@@ -136,6 +136,14 @@ module.exports = ({ bundle, sessionId }) => ({
chunkFilename: generateName(bundle),
publicPath: '',
},
+ resolve:
+ bundle === 'apps'
+ ? {
+ fallback: {
+ buffer: require.resolve('buffer/'),
+ },
+ }
+ : undefined,
plugins: [
new WebpackManifestPlugin({
fileName: `manifest.${bundle}.json`,
@@ -145,6 +153,9 @@ module.exports = ({ bundle, sessionId }) => ({
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
+ new webpack.ProvidePlugin({
+ Buffer: ['buffer', 'Buffer'],
+ }),
]
: []),
...(DEV
diff --git a/dotcom-rendering/src/web/components/Follow.importable.tsx b/dotcom-rendering/src/web/components/Follow.importable.tsx
index 68a3bae2034..a1037991dbf 100644
--- a/dotcom-rendering/src/web/components/Follow.importable.tsx
+++ b/dotcom-rendering/src/web/components/Follow.importable.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/react';
import { Topic } from '@guardian/bridget/Topic';
import { ArticleSpecial } from '@guardian/libs';
import { space, textSans } from '@guardian/source-foundations';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { notificationsClient } from '../../app/native/nativeApi';
import type { Palette } from '../../types/palette';
import type { TagType } from '../../types/tag';
@@ -60,7 +60,7 @@ const buttonStyles = (palette: Palette) => css`
${
/*darkModeCss`
color: ${text.followDark(format)};
-`*/ ''
+ `*/ ''
}
`;
@@ -107,7 +107,7 @@ export const FollowButton = ({
.catch(() => {});
}, [contributor]);
- const onFollowingClick = () => {
+ const onFollowingClick = useCallback(() => {
const topic = new Topic({
id: contributor.id,
displayName: contributor.title,
@@ -125,7 +125,7 @@ export const FollowButton = ({
.then(() => setIsFollowing(true))
.catch(() => {});
}
- };
+ }, [contributor, isFollowing]);
return (