diff --git a/workspaces/client/src/model/cms/ActivityPost.ts b/workspaces/client/src/model/cms/ActivityPost.ts index 3290036a8d8a56c33728eebcc4470648843964e5..2d391d250d39aa21b55155dbe7996c4997e2075f 100644 --- a/workspaces/client/src/model/cms/ActivityPost.ts +++ b/workspaces/client/src/model/cms/ActivityPost.ts @@ -3,6 +3,7 @@ import * as _ from 'lodash'; * Defined in Swagger as '#/definitions/ActivityPost' */ import { SortDirection } from '@genesys/client/model/Page'; +import { stripHtml } from '@genesys/client/utilities'; class ActivityPost { public active: boolean; @@ -46,20 +47,17 @@ class ActivityPost { }; public static htmlTitleToText = (activityPost: ActivityPost, config: { length?: number, lowerCase?: boolean } = {length: 60, lowerCase: false}) => { - - const textTitle = _.unescape(activityPost.title) - .replace(/&#([0-9]{1,7});/g, (g, m1) => String.fromCharCode(parseInt (m1, 10))) - .replace(/<.*?>/g, '') - .slice(0, config.length); - + const textTitle = stripHtml(activityPost.title).slice(0, config.length); return config.lowerCase ? textTitle.toLocaleLowerCase() : textTitle; - } + }; + + public static htmlSummaryToText = (activityPost: ActivityPost, config: { length?: number } = {length: 200}) => { + return stripHtml(activityPost.summary).slice(0, config.length).trim(); + }; public static htmlTitleToUrl = (activityPost: ActivityPost, config: { length?: number, replacer?: string } = {length: 60, replacer: '-'}) => { - const textTitle = _.unescape(activityPost.title) - .replace(/&#([0-9]{1,7});/g, (g, m1) => String.fromCharCode(parseInt (m1, 10))) - .replace(/<.*?>/g, '') + const textTitle = stripHtml(activityPost.title) .slice(0, config.length) .replace(/[^\w]+/g, config.replacer) .replace(`${ config.replacer }+$`, '') diff --git a/workspaces/client/src/utilities/index.ts b/workspaces/client/src/utilities/index.ts index 6e77d64a739ef7c7573cf74a5e394e97f57f7805..ccdb2e03fc271d310fef98e23aa5a136297689a2 100644 --- a/workspaces/client/src/utilities/index.ts +++ b/workspaces/client/src/utilities/index.ts @@ -371,3 +371,10 @@ export function dereferenceReferences3(content: any[], referenceMaps: { [key: st // console.log('End caches', refs); return content; } + +export function stripHtml(markup: string) { + return _ + .unescape(markup) + .replace(/&#([0-9]{1,7});/g, (g, m1) => String.fromCharCode(parseInt (m1, 10))) + .replace(/<.*?>/g, ''); +} diff --git a/workspaces/ui-express/entrypoints/client.tsx b/workspaces/ui-express/entrypoints/client.tsx index bc36067cad4b9300add276e711ee98d211991686..cffaf6185ea16f6da7fff2ff32b877f4a176450b 100644 --- a/workspaces/ui-express/entrypoints/client.tsx +++ b/workspaces/ui-express/entrypoints/client.tsx @@ -18,6 +18,7 @@ import detectLocaleFromPath from '../server/middleware/detectLocaleFromPath'; import getDir from '../server/middleware/detectDirection'; import { receiveLang } from 'actions/applicationConfig'; import * as moment from 'moment'; +import { HelmetProvider } from 'react-helmet-async'; // JSS & MUI import { MuiThemeProvider } from '@material-ui/core/styles'; @@ -68,7 +69,9 @@ if (__PRELOADED_STATE__ === undefined) { - { renderRoutes(routes) } + + { renderRoutes(routes) } + @@ -87,7 +90,9 @@ if (__PRELOADED_STATE__ === undefined) { - { renderRoutes(routes) } + + { renderRoutes(routes) } + diff --git a/workspaces/ui-express/package.json b/workspaces/ui-express/package.json index 8a3739cf094786d9d6f16fb843db30f135efbc6d..f7e7f458cbcd50ece7bf853df48dbd2a474fd2e8 100644 --- a/workspaces/ui-express/package.json +++ b/workspaces/ui-express/package.json @@ -90,6 +90,7 @@ "react-fontawesome": "^1.7.1", "react-google-login": "5.0.7", "react-google-recaptcha": "^2.0.1", + "react-helmet-async": "^1.0.7", "react-i18next": "^11.3.4", "react-leaflet": "^2.6.3", "react-leaflet-control": "^2.1.2", diff --git a/workspaces/ui-express/server/middleware/prerenderer.tsx b/workspaces/ui-express/server/middleware/prerenderer.tsx index 5ed0338dd7a0cb69080cbe403e00f2525c9df10c..7cec133c71de17f9910b9088820cd0bcb262eee1 100644 --- a/workspaces/ui-express/server/middleware/prerenderer.tsx +++ b/workspaces/ui-express/server/middleware/prerenderer.tsx @@ -15,6 +15,7 @@ import thunk from 'redux-thunk'; import rootReducer from 'reducers'; import languages from 'data/Languages'; import * as moment from 'moment'; +import { FilledContext, HelmetProvider } from 'react-helmet-async'; // import { create as createJss } from 'jss'; // import jssPreset from 'jss-preset-default'; @@ -95,6 +96,7 @@ const prerenderer = (html, errHtml) => (req, res) => { console.log(` modules.push(moduleName) }> @@ -102,9 +104,11 @@ const prerenderer = (html, errHtml) => (req, res) => { - - { renderRoutes(routes) } - + + + { renderRoutes(routes) } + + @@ -114,7 +118,8 @@ const prerenderer = (html, errHtml) => (req, res) => { const componentHTML = renderToString(InitialView); const initialState = store.getState(); - const titleState = store.getState().pageTitle.title; + + const { helmet } = helmetContext as FilledContext; // console.log('react-loadable modules', modules); const bundles = getBundles(reactLoadableStats, modules); @@ -138,12 +143,6 @@ const prerenderer = (html, errHtml) => (req, res) => { } }); - // HTML meta for activity posts - const activityPost = store.getState().cms.public.activityPost; - const currentPost = activityPost && activityPost.currentPost && activityPost.currentPost.data; - const metaDescriptionText = currentPost ? currentPost.summary : null; - const metaDescription = metaDescriptionText ? `` : ''; - return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG|HEADLINKS|BUNDLECSS|BUNDLESCRIPTS|DESCRIPTION)/g, (match, x) => { // console.log(`Injecting ${match}`); switch (match) { @@ -159,12 +158,12 @@ const prerenderer = (html, errHtml) => (req, res) => { case 'SERVER_RENDERED_CSS': return sheets.toString(); case 'SERVER_RENDERED_STATE': return serialize(initialState, {isJSON: true}); case 'SERVER_RENDERED_HTML': return componentHTML; - case 'SERVER_RENDERED_TITLE': return titleState; + case 'SERVER_RENDERED_TITLE': return helmet.title.toString(); + case 'SERVER_RENDERED_DESCRIPTION': return helmet.meta.toString(); case 'SERVER_RENDERED_I18NSTORE': return serialize(initialI18nStore, {isJSON: true}); case 'SERVER_RENDERED_DIR': return serialize(getDir(initialLanguage), {isJSON: false}); case 'SERVER_RENDERED_LANG': return serialize(initialLanguage, {isJSON: false}); case 'SERVER_RENDERED_HEADLINKS': return languageLinks.join(''); - case 'SERVER_RENDERED_DESCRIPTION': return metaDescription; default: console.log(`Unrecognized variable in ssr-template.html: ${match}`); return ''; } }); diff --git a/workspaces/ui-express/server/ssr-template.html b/workspaces/ui-express/server/ssr-template.html index b79bad2dd44b7b9b8f6c18510c22ed6dcef63194..1fc6669a940c4a55e554fc59320a1ac9424aa343 100644 --- a/workspaces/ui-express/server/ssr-template.html +++ b/workspaces/ui-express/server/ssr-template.html @@ -2,7 +2,7 @@ - SERVER_RENDERED_TITLE + SERVER_RENDERED_TITLE SERVER_RENDERED_HEADLINKS diff --git a/workspaces/ui-express/src/actions/pageTitle.ts b/workspaces/ui-express/src/actions/pageTitle.ts deleted file mode 100644 index cf036a38da09f9fbaf9f88c3a36716b2a9caaff8..0000000000000000000000000000000000000000 --- a/workspaces/ui-express/src/actions/pageTitle.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SET_PAGE_TITLE } from 'constants/pageTitle'; - -export const setPageTitle = (title: string) => (dispatch) => { - dispatch({type: SET_PAGE_TITLE, payload: title}); -}; diff --git a/workspaces/ui-express/src/cms/ui/ActivityPostDisplayPage.tsx b/workspaces/ui-express/src/cms/ui/ActivityPostDisplayPage.tsx index 369ce7c4d6cada2fa02f4b3db01ff94727f1faa4..84fee138e3ab49d0b0ce2fe705920080edbc4bf6 100644 --- a/workspaces/ui-express/src/cms/ui/ActivityPostDisplayPage.tsx +++ b/workspaces/ui-express/src/cms/ui/ActivityPostDisplayPage.tsx @@ -63,7 +63,8 @@ class ActivityPostDisplayPage extends React.Component public render() { const { lastNews: apiCall, activityPost, loading } = this.props; - const title = !loading && activityPost && ActivityPost.htmlTitleToText(activityPost); + const title = activityPost && ActivityPost.htmlTitleToText(activityPost); + const description = activityPost && activityPost.summary && ActivityPost.htmlSummaryToText(activityPost); const lastNewsMenu = new Menu(); if (apiCall && apiCall.data) { @@ -77,12 +78,12 @@ class ActivityPostDisplayPage extends React.Component } return ( + { !apiCall || !apiCall.data || loading || !activityPost ? ( ) : ( - }/> diff --git a/workspaces/ui-express/src/cms/ui/admin/ActivityPostEditPage.tsx b/workspaces/ui-express/src/cms/ui/admin/ActivityPostEditPage.tsx index 4e8eee6708dd0cce4f33107cde12d82d7f35bde7..a8e186d1fe58dab2e68b3c3d54c93577eb42d433 100644 --- a/workspaces/ui-express/src/cms/ui/admin/ActivityPostEditPage.tsx +++ b/workspaces/ui-express/src/cms/ui/admin/ActivityPostEditPage.tsx @@ -107,7 +107,10 @@ class ActivityPostEditPage extends React.Component { return (
- + diff --git a/workspaces/ui-express/src/constants/pageTitle.ts b/workspaces/ui-express/src/constants/pageTitle.ts deleted file mode 100644 index 1721f760fdce365f816e828d841ecc0e87173b9a..0000000000000000000000000000000000000000 --- a/workspaces/ui-express/src/constants/pageTitle.ts +++ /dev/null @@ -1 +0,0 @@ -export const SET_PAGE_TITLE = 'SET_PAGE_TITLE'; diff --git a/workspaces/ui-express/src/reducers/index.ts b/workspaces/ui-express/src/reducers/index.ts index 02f9069cd79a7698a3310347c19dcee3064c6de8..09fe7444b5d64c5dc48369f6954383a3b60dddd3 100644 --- a/workspaces/ui-express/src/reducers/index.ts +++ b/workspaces/ui-express/src/reducers/index.ts @@ -5,7 +5,6 @@ import login from './login'; import serverInfo from './serverInfo'; import appMounted from './appMounted'; import historyReducer from './history'; -import pageTitle from './pageTitle'; import snackbar from './snackbar'; import filterCode from './filterCode'; import decoder from './decoder'; @@ -41,7 +40,6 @@ const rootReducer = (history?) => combineReducers({ history: historyReducer, login, serverInfo, - pageTitle, applicationConfig, snackbar, filterCode, diff --git a/workspaces/ui-express/src/reducers/pageTitle.ts b/workspaces/ui-express/src/reducers/pageTitle.ts deleted file mode 100644 index 5a3ae553fc69f71200b787dd7660796bc409f5bf..0000000000000000000000000000000000000000 --- a/workspaces/ui-express/src/reducers/pageTitle.ts +++ /dev/null @@ -1,26 +0,0 @@ -import update from 'immutability-helper'; - -import { SET_PAGE_TITLE } from 'constants/pageTitle'; - -const INITIAL_STATE: { - title: string, -} = { - title: 'Genesys PGR', -}; - -export default (state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) => { - - switch (action.type) { - case SET_PAGE_TITLE: { - if (typeof window !== 'undefined') { - window.document.title = action.payload; - } - return update(state, { - title: {$set: action.payload}, - }); - } - - default: - return state; - } -} diff --git a/workspaces/ui-express/src/ui/App.tsx b/workspaces/ui-express/src/ui/App.tsx index d38a20dce150afcf3fadaa0ff068ffefb28bda19..e46e1c3b0313932ea86f3fef5c79d011f1347ff8 100644 --- a/workspaces/ui-express/src/ui/App.tsx +++ b/workspaces/ui-express/src/ui/App.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { WithTranslation, withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import * as _ from 'lodash'; import { updateHistory } from 'actions/history'; import { loadCrops } from 'crop/actions/public'; @@ -20,7 +19,6 @@ import Snackbar from 'ui/common/snackbar/Snackbar'; import renderRoutes from 'ui/renderRoutes'; import Crop from '@genesys/client/model/genesys/Crop'; import ApiCall from '@genesys/client/model/ApiCall'; -import ActivityPost from '@genesys/client/model/cms/ActivityPost'; interface IAppProps extends React.ClassAttributes, WithTranslation { @@ -38,7 +36,6 @@ interface IAppProps extends React.ClassAttributes, WithTranslation { serverInfoRequest: any; checkSoftwareVersion: any; initMyMaps: any; - currentPost: ApiCall; refreshIso3Decodings: (lang: string) => void; lang: string; } @@ -68,8 +65,8 @@ class App extends React.Component { } public componentDidUpdate(prevProps: IAppProps) { - const {updateHistory, currentPost: prevPost, location: prevLocation} = prevProps; - const {currentPost, location} = this.props; + const {updateHistory, location: prevLocation} = prevProps; + const {location} = this.props; if (prevLocation !== null && location !== null) { if (prevLocation !== location) { if (typeof window !== 'undefined') { @@ -87,24 +84,6 @@ class App extends React.Component { } } - if (currentPost && currentPost.data && prevPost && !_.isEqual(prevPost, currentPost)) { - const metaDescription = document.querySelector('meta[name="description"]'); - if (currentPost.data.summary) { - if (metaDescription) { - metaDescription.setAttribute('content', currentPost.data.summary); - } else { - const descriptionEl = document.createElement('meta'); - descriptionEl.setAttribute('name', 'description'); - descriptionEl.setAttribute('content', currentPost.data.summary); - document.getElementsByTagName('head')[0].appendChild(descriptionEl); - } - } else { - if (metaDescription) { - metaDescription.remove(); - } - } - } - const {countryCodes, loadIso3Decodings, i18n, tReady} = this.props; if (tReady && (!countryCodes || (!countryCodes.loading && !countryCodes.error && !countryCodes.data))) { loadIso3Decodings(i18n.language); @@ -128,7 +107,6 @@ const mapStateToProps = (state) => ({ crops: state.crop.public.list ? state.crop.public.list.data : undefined, serverInfo: state.serverInfo.data, lang: state.applicationConfig.lang, - currentPost: state.cms.public.activityPost.currentPost, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ diff --git a/workspaces/ui-express/src/ui/common/PageTitle.ts b/workspaces/ui-express/src/ui/common/PageTitle.ts deleted file mode 100644 index 5c70587fb46f0fbb6c52822eca47a5ba48ba3b35..0000000000000000000000000000000000000000 --- a/workspaces/ui-express/src/ui/common/PageTitle.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import { setPageTitle } from 'actions/pageTitle'; - -interface IPageTitleProps extends React.ClassAttributes { - title: string; - setPageTitle?: (title: string) => void; -} - -class PageTitle extends React.Component { - - public constructor(props) { - super(props); - const { setPageTitle, title } = props; - if (title && setPageTitle) { - setPageTitle(title.split('*').join('')); - } - } - - public componentDidUpdate(prevProps, prevState, snapshot) { - const { title: oldTitle } = prevProps; - const { title, setPageTitle} = this.props; - - if (title && setPageTitle) { - if (title !== oldTitle) { - setPageTitle(title.split('*').join('')); - } - } - } - - public render() { - return null; - } -} - -const mapDispatchToProps = (dispatch) => bindActionCreators({ - setPageTitle, -}, dispatch); - - -export default connect(null, mapDispatchToProps)(PageTitle); diff --git a/workspaces/ui-express/src/ui/common/PageTitle.tsx b/workspaces/ui-express/src/ui/common/PageTitle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aeaa9126e4fd348c41164be2e8741ea937237e9c --- /dev/null +++ b/workspaces/ui-express/src/ui/common/PageTitle.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { WithTranslation, withTranslation } from 'react-i18next'; +import { Helmet } from 'react-helmet-async'; +import { stripHtml } from '@genesys/client/utilities'; + +interface IPageTitle extends React.ClassAttributes, WithTranslation { + title: string; + description?: string; +} + +class PageTitle extends React.Component { + public constructor(props, context) { + super(props, context); + } + + public render() { + const { title, description, t } = this.props; + + return ( + + { t(stripHtml(title)) } + { description && } + + ); + } +} + +export default withTranslation()(PageTitle); diff --git a/yarn.lock b/yarn.lock index 705dacd8dcf318555511cf6437f53773c74d7907..ce91b62c5c6c4cb37988b9e25f0dd37b06df2d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,7 +151,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== @@ -11247,6 +11247,11 @@ react-dom@^16.13.1: prop-types "^15.6.2" scheduler "^0.19.1" +react-fast-compare@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-fontawesome@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/react-fontawesome/-/react-fontawesome-1.7.1.tgz#f74f5a338fef3ee3b379820109c1cba47290f035" @@ -11270,6 +11275,17 @@ react-google-recaptcha@^2.0.1: prop-types "^15.5.0" react-async-script "^1.1.1" +react-helmet-async@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.7.tgz#b988fbc3abdc4b704982bb74b9cb4a08fcf062c1" + integrity sha512-By90p5uxAriGukbyejq2poK41DwTxpNWOpOjN8mIyX/BKrCd3+sXZ5pHUZXjHyjR5OYS7PGsOD9dbM61YxfFmA== + dependencies: + "@babel/runtime" "^7.11.2" + invariant "^2.2.4" + prop-types "^15.7.2" + react-fast-compare "^3.2.0" + shallowequal "^1.1.0" + react-hot-loader@^4.12.20: version "4.12.21" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975"