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 ( + ); +}; diff --git a/dotcom-rendering/src/web/components/ImageBlockComponent.stories.tsx b/dotcom-rendering/src/web/components/ImageBlockComponent.stories.tsx index 310e710b6d0..9f7a6c61a88 100644 --- a/dotcom-rendering/src/web/components/ImageBlockComponent.stories.tsx +++ b/dotcom-rendering/src/web/components/ImageBlockComponent.stories.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { ArticleDesign, ArticleDisplay, ArticlePillar } from '@guardian/libs'; +import { Platform } from '../../types/platform'; import { Figure } from './Figure'; import { Flex } from './Flex'; import { ImageBlockComponent } from './ImageBlockComponent'; @@ -61,6 +62,7 @@ export const StandardArticle = () => { role="inline" > { role="immersive" > { role="showcase" > { role="thumbnail" > { role="supporting" > { role="inline" > { role="inline" > { role="inline" > { role="immersive" > { role="showcase" > { role="halfWidth" > { role="halfWidth" > { role="halfWidth" > ( ); export const ImageComponent = ({ + platform, element, format, hideCaption, @@ -319,6 +324,19 @@ export const ImageComponent = ({ isLazy={!isMainMedia} isMainMedia={isMainMedia} /> + + + {!!title && ( )} @@ -350,6 +368,19 @@ export const ImageComponent = ({ isLazy={!isMainMedia} isMainMedia={isMainMedia} /> + + + {typeof starRating === 'number' && ( )} @@ -384,6 +415,19 @@ export const ImageComponent = ({ isLazy={!isMainMedia} isMainMedia={isMainMedia} /> + + + {isMainMedia && ( // Below tablet, main media images show an info toggle at the bottom right of // the image which, when clicked, toggles the caption as an overlay diff --git a/dotcom-rendering/src/web/components/MiniCard.tsx b/dotcom-rendering/src/web/components/MiniCard.tsx index 16b09583900..7a8412f1410 100644 --- a/dotcom-rendering/src/web/components/MiniCard.tsx +++ b/dotcom-rendering/src/web/components/MiniCard.tsx @@ -3,7 +3,7 @@ import { body, breakpoints } from '@guardian/source-foundations'; import { Link } from '@guardian/source-react-components'; import type { DCRContainerPalette } from 'src/types/front'; import type { TrailType } from 'src/types/trails'; -import { ContainerOverrides } from '../../types/palette'; +import type { ContainerOverrides } from '../../types/palette'; import { decideContainerOverrides } from '../lib/decideContainerOverrides'; import { generateSources } from './Picture'; diff --git a/dotcom-rendering/src/web/components/Picture.tsx b/dotcom-rendering/src/web/components/Picture.tsx index 9f43a690e7c..12e1d88f8f7 100644 --- a/dotcom-rendering/src/web/components/Picture.tsx +++ b/dotcom-rendering/src/web/components/Picture.tsx @@ -17,6 +17,8 @@ type Props = { width: string; isMainMedia?: boolean; isLazy?: boolean; + caption?: string; + credit?: string; }; export type ImageWidthType = { breakpoint: number; width: number }; @@ -333,7 +335,6 @@ export const Picture = ({ ); })} - {alt} { // there is no straightforward way to tell if a React element is null by direct // inspection. export const renderElement = ({ + platform, format, element, adTargeting, @@ -322,6 +325,7 @@ export const renderElement = ({ case 'model.dotcomrendering.pageElements.ImageBlockElement': return ( Date: Tue, 20 Dec 2022 13:06:39 +0000 Subject: [PATCH 11/24] `app` => `mobile-apps` --- dotcom-rendering/src/server/dev-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/server/dev-server.ts b/dotcom-rendering/src/server/dev-server.ts index 03c2b0268e0..53375f21a7a 100644 --- a/dotcom-rendering/src/server/dev-server.ts +++ b/dotcom-rendering/src/server/dev-server.ts @@ -39,7 +39,7 @@ export const devServer = (): Handler => { return handleFront(req, res, next); case '/FrontJSON': return handleFrontJson(req, res, next); - case '/app/Article': + case '/mobile-apps/Article': return handleAppArticle(req, res, next); default: { if (req.url.match(ARTICLE_URL)) { From 6e6d65bd3dfe599922ae280e52019210d0c8cd7b Mon Sep 17 00:00:00 2001 From: Jamie B <53781962+JamieB-gu@users.noreply.github.com> Date: Tue, 20 Dec 2022 16:32:40 +0000 Subject: [PATCH 12/24] Prop drill platform --- .vscode/settings.json | 3 +- .../src/amp/components/Elements.tsx | 1 - .../src/app/layouts/StandardLayout.tsx | 1 + .../src/app/server/articleToHtml.tsx | 34 ++++++++----------- .../src/web/components/ArticleBody.tsx | 1 + .../components/ArticleHeadline.stories.tsx | 6 ++++ .../src/web/components/LiveBlock.stories.tsx | 13 +++++++ .../src/web/components/LiveBlock.tsx | 4 +++ .../src/web/components/MainMedia.tsx | 4 +++ .../MultiImageBlockComponent.stories.tsx | 9 +++++ .../components/MultiImageBlockComponent.tsx | 13 +++++++ .../src/web/components/Picture.tsx | 2 -- .../src/web/components/PinnedPost.stories.tsx | 8 +++++ .../src/web/layouts/CommentLayout.tsx | 1 + .../web/layouts/FullPageInteractiveLayout.tsx | 3 +- .../src/web/layouts/InteractiveLayout.tsx | 1 + .../src/web/layouts/LiveLayout.tsx | 1 + .../web/layouts/NewsletterSignupLayout.tsx | 2 ++ .../src/web/layouts/ShowcaseLayout.tsx | 1 + .../src/web/layouts/StandardLayout.tsx | 1 + .../web/layouts/headers/ImmersiveHeader.tsx | 2 ++ .../src/web/lib/ArticleRenderer.tsx | 4 +++ .../src/web/lib/LiveBlogRenderer.tsx | 2 ++ .../src/web/lib/renderElement.tsx | 3 ++ .../src/web/server/blocksToHtml.tsx | 1 - 25 files changed, 95 insertions(+), 26 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d09accb6442..4e8f7c9a5d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,5 +24,6 @@ ], "git.branchProtection": [ "main" - ] + ], + "typescript.preferences.importModuleSpecifier": "relative", } diff --git a/dotcom-rendering/src/amp/components/Elements.tsx b/dotcom-rendering/src/amp/components/Elements.tsx index 9f64205da6b..5f9abe7e98e 100644 --- a/dotcom-rendering/src/amp/components/Elements.tsx +++ b/dotcom-rendering/src/amp/components/Elements.tsx @@ -1,5 +1,4 @@ import type { Switches } from 'src/types/config'; -import { Platform } from 'src/types/platform'; import { NotRenderableInDCR } from '../../lib/errors/not-renderable-in-dcr'; import type { CAPIElement } from '../../types/content'; import type { TagType } from '../../types/tag'; diff --git a/dotcom-rendering/src/app/layouts/StandardLayout.tsx b/dotcom-rendering/src/app/layouts/StandardLayout.tsx index 0308f422e45..929ca652de4 100644 --- a/dotcom-rendering/src/app/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/app/layouts/StandardLayout.tsx @@ -434,6 +434,7 @@ export const StandardLayout = ({ CAPIArticle, format }: Props) => { switches={CAPIArticle.config.switches} isAdFreeUser={CAPIArticle.isAdFreeUser} isSensitive={CAPIArticle.config.isSensitive} + platform={Platform.Apps} />
diff --git a/dotcom-rendering/src/app/server/articleToHtml.tsx b/dotcom-rendering/src/app/server/articleToHtml.tsx index 5a9d5695307..fade5b68a51 100644 --- a/dotcom-rendering/src/app/server/articleToHtml.tsx +++ b/dotcom-rendering/src/app/server/articleToHtml.tsx @@ -4,14 +4,8 @@ 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, getAppScript, - getScriptsFromManifest, } from '../../lib/assets'; // import { makeWindowGuardian } from '../../model/window-guardian'; import type { CAPIElement } from '../../types/content'; @@ -66,13 +60,13 @@ export const articleToHtml = ({ article }: Props): string => { // 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 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) => @@ -80,10 +74,10 @@ export const articleToHtml = ({ article }: Props): string => { 'model.dotcomrendering.pageElements.TweetBlockElement', ); - const shouldServeVariantBundle: boolean = [ - BUILD_VARIANT, - article.config.abTests[dcrJavascriptBundle('Variant')] === 'variant', - ].every(Boolean); + // const shouldServeVariantBundle: boolean = [ + // BUILD_VARIANT, + // article.config.abTests[dcrJavascriptBundle('Variant')] === 'variant', + // ].every(Boolean); /** * This function returns an array of files found in the manifests @@ -91,9 +85,9 @@ export const articleToHtml = ({ article }: Props): string => { * * @see getScriptsFromManifest */ - const getScriptArrayFromFile = getScriptsFromManifest( - shouldServeVariantBundle, - ); + // const getScriptArrayFromFile = getScriptsFromManifest( + // shouldServeVariantBundle, + // ); /** * The highest priority scripts. diff --git a/dotcom-rendering/src/web/components/ArticleBody.tsx b/dotcom-rendering/src/web/components/ArticleBody.tsx index b3e57358ec6..15c6c593557 100644 --- a/dotcom-rendering/src/web/components/ArticleBody.tsx +++ b/dotcom-rendering/src/web/components/ArticleBody.tsx @@ -262,6 +262,7 @@ const ArticleBody = ( isDev={isDev} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} + platform={platform} /> ); diff --git a/dotcom-rendering/src/web/components/ArticleHeadline.stories.tsx b/dotcom-rendering/src/web/components/ArticleHeadline.stories.tsx index 860614cf5aa..434a07d5e88 100644 --- a/dotcom-rendering/src/web/components/ArticleHeadline.stories.tsx +++ b/dotcom-rendering/src/web/components/ArticleHeadline.stories.tsx @@ -6,6 +6,7 @@ import { ArticleSpecial, } from '@guardian/libs'; import { news } from '@guardian/source-foundations'; +import { Platform } from '../../types/platform'; import { ArticleContainer } from './ArticleContainer'; import { ArticleHeadline } from './ArticleHeadline'; import { mainMediaElements } from './ArticleHeadline.mocks'; @@ -108,6 +109,7 @@ export const ShowcaseInterview = () => { isAdFreeUser={false} isSensitive={false} switches={{}} + platform={Platform.Web} />
@@ -152,6 +154,7 @@ export const ShowcaseInterviewNobyline = () => { isAdFreeUser={false} isSensitive={false} switches={{}} + platform={Platform.Web} /> @@ -196,6 +199,7 @@ export const Interview = () => { isAdFreeUser={false} isSensitive={false} switches={{}} + platform={Platform.Web} /> @@ -238,6 +242,7 @@ export const InterviewSpecialReport = () => { isAdFreeUser={false} isSensitive={false} switches={{}} + platform={Platform.Web} /> @@ -282,6 +287,7 @@ export const InterviewNoByline = () => { isAdFreeUser={false} isSensitive={false} switches={{}} + platform={Platform.Web} /> diff --git a/dotcom-rendering/src/web/components/LiveBlock.stories.tsx b/dotcom-rendering/src/web/components/LiveBlock.stories.tsx index 74a724509e7..5280a33fd45 100644 --- a/dotcom-rendering/src/web/components/LiveBlock.stories.tsx +++ b/dotcom-rendering/src/web/components/LiveBlock.stories.tsx @@ -4,6 +4,7 @@ import { breakpoints, from } from '@guardian/source-foundations'; import { liveBlock } from '../../../fixtures/manual/liveBlock'; import { images } from '../../../fixtures/generated/images'; import { LiveBlock } from './LiveBlock'; +import { Platform } from '../../types/platform'; const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( @@ -81,6 +82,7 @@ export const VideoAsSecond = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -130,6 +132,7 @@ export const Title = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -200,6 +203,7 @@ export const Video = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -245,6 +249,7 @@ export const RichLink = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -281,6 +286,7 @@ export const FirstImage = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -341,6 +347,7 @@ export const ImageRoles = () => { ajaxUrl="" switches={{}} isPinnedPost={false} + platform={Platform.Web} isAdFreeUser={false} isSensitive={false} /> @@ -392,6 +399,7 @@ export const Thumbnail = () => { ajaxUrl="" switches={{}} isPinnedPost={false} + platform={Platform.Web} isAdFreeUser={false} isSensitive={false} /> @@ -431,6 +439,7 @@ export const ImageAndTitle = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -464,6 +473,7 @@ export const Updated = () => { isSensitive={false} switches={{}} isPinnedPost={false} + platform={Platform.Web} /> ); @@ -501,6 +511,7 @@ export const Contributor = () => { ajaxUrl="" switches={{}} isPinnedPost={false} + platform={Platform.Web} isAdFreeUser={false} isSensitive={false} /> @@ -536,6 +547,7 @@ export const NoAvatar = () => { ajaxUrl="" switches={{}} isPinnedPost={false} + platform={Platform.Web} isAdFreeUser={false} isSensitive={false} /> @@ -574,6 +586,7 @@ export const TitleAndContributor = () => { ajaxUrl="" switches={{}} isPinnedPost={false} + platform={Platform.Web} isAdFreeUser={false} isSensitive={false} /> diff --git a/dotcom-rendering/src/web/components/LiveBlock.tsx b/dotcom-rendering/src/web/components/LiveBlock.tsx index 6d01337290d..d234599f7b2 100644 --- a/dotcom-rendering/src/web/components/LiveBlock.tsx +++ b/dotcom-rendering/src/web/components/LiveBlock.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; // eslint-disable-next-line import/no-extraneous-dependencies -- it’s a yarn workspace import LiveBlockContainer from '@guardian/common-rendering/src/components/liveBlockContainer'; +import { Platform } from '../../types/platform'; import type { Switches } from '../../types/config'; import { RenderArticleElement } from '../lib/renderElement'; import { LastUpdated } from './LastUpdated'; @@ -20,6 +21,7 @@ type Props = { isLiveUpdate?: boolean; isPinnedPost: boolean; pinnedPostId?: string; + platform: Platform; }; export const LiveBlock = ({ @@ -36,6 +38,7 @@ export const LiveBlock = ({ isLiveUpdate, isPinnedPost, pinnedPostId, + platform, }: Props) => { if (block.elements.length === 0) return null; // Decide if the block has been updated or not @@ -80,6 +83,7 @@ export const LiveBlock = ({ isSensitive={isSensitive} switches={switches} isPinnedPost={isPinnedPost} + platform={platform} /> ))}