Commit 0ffacb5b authored by Matija Obreza's avatar Matija Obreza

Merge branch '748-html-metadata' into 'master'

HTML metadata

Closes #748

See merge request genesys-pgr/genesys-ui!726
parents f6c7724f 15e2144a
......@@ -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 }+$`, '')
......
......@@ -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, '');
}
......@@ -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) {
<ConnectedRouter history={ history }>
<I18nextProvider i18n={ i18nClient }>
<MuiThemeProvider theme={ theme(direction) }>
{ renderRoutes(routes) }
<HelmetProvider>
{ renderRoutes(routes) }
</HelmetProvider>
</MuiThemeProvider>
</I18nextProvider>
</ConnectedRouter>
......@@ -87,7 +90,9 @@ if (__PRELOADED_STATE__ === undefined) {
<ConnectedRouter history={ history }>
<SsrI18nProvider i18n={ i18nClient } initialLanguage={ initialLanguage } initialI18nStore={ initialI18nStore }>
<MuiThemeProvider theme={ theme(direction) }>
{ renderRoutes(routes) }
<HelmetProvider>
{ renderRoutes(routes) }
</HelmetProvider>
</MuiThemeProvider>
</SsrI18nProvider>
</ConnectedRouter>
......
......@@ -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",
......
......@@ -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(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
const direction = getDir(initialLanguage);
const modules = [];
const helmetContext = {};
const InitialView = (
<Loadable.Capture report={ (moduleName) => modules.push(moduleName) }>
......@@ -102,9 +104,11 @@ const prerenderer = (html, errHtml) => (req, res) => {
<StylesProvider generateClassName={ generateClassName } sheetsRegistry={ sheets }>
<MuiThemeProvider theme={ theme(direction) }>
<I18nextProvider i18n={ req.i18n }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
<HelmetProvider context={ helmetContext }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
</HelmetProvider>
</I18nextProvider>
</MuiThemeProvider>
</StylesProvider>
......@@ -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 ? `<meta name="description" content="${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 '';
}
});
......
......@@ -2,7 +2,7 @@
<html lang=SERVER_RENDERED_LANG dir=SERVER_RENDERED_DIR>
<head>
<title>SERVER_RENDERED_TITLE</title>
SERVER_RENDERED_TITLE
SERVER_RENDERED_HEADLINKS
<base href="FRONTEND_PATH" />
<meta charset="UTF-8" />
......
import { SET_PAGE_TITLE } from 'constants/pageTitle';
export const setPageTitle = (title: string) => (dispatch) => {
dispatch({type: SET_PAGE_TITLE, payload: title});
};
......@@ -63,7 +63,8 @@ class ActivityPostDisplayPage extends React.Component<IActivityPostDisplayPage>
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<IActivityPostDisplayPage>
}
return (
<PageLayout withFooter>
<PageTitle title={ title } description={ description }/>
<ScrollToTopOnMount/>
{ !apiCall || !apiCall.data || loading || !activityPost ? (
<Loading/>
) : (
<Grid container style={ {height: '100%'} }>
<PageTitle title={ title }/>
<ContentHeader title={ <span dangerouslySetInnerHTML={ {__html: activityPost.title} }/> }/>
<ArticleSection title={ title } body={ activityPost.body }/>
<MenuStepper menu={ lastNewsMenu }/>
......
......@@ -107,7 +107,10 @@ class ActivityPostEditPage extends React.Component<IArticleEditPageProps, any> {
return (
<div>
<PageTitle title={ activityPost.title ? ActivityPost.htmlTitleToText(activityPost) : t('cms.admin.p.edit.activityPostPageTitle') }/>
<PageTitle
title={ activityPost.title ? ActivityPost.htmlTitleToText(activityPost) : t('cms.admin.p.edit.activityPostPageTitle') }
description={ activityPost.summary ? ActivityPost.htmlSummaryToText(activityPost) : null }
/>
<ContentHeaderWithButton
title={
<span dangerouslySetInnerHTML={ { __html: isEdit ? activityPost.title : editingTitle } }/>
......
export const SET_PAGE_TITLE = 'SET_PAGE_TITLE';
......@@ -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,
......
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;
}
}
......@@ -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<any>, WithTranslation {
......@@ -38,7 +36,6 @@ interface IAppProps extends React.ClassAttributes<any>, WithTranslation {
serverInfoRequest: any;
checkSoftwareVersion: any;
initMyMaps: any;
currentPost: ApiCall<ActivityPost>;
refreshIso3Decodings: (lang: string) => void;
lang: string;
}
......@@ -68,8 +65,8 @@ class App extends React.Component<IAppProps, any> {
}
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<IAppProps, any> {
}
}
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({
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { setPageTitle } from 'actions/pageTitle';
interface IPageTitleProps extends React.ClassAttributes<any> {
title: string;
setPageTitle?: (title: string) => void;
}
class PageTitle extends React.Component<IPageTitleProps> {
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);
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<any>, WithTranslation {
title: string;
description?: string;
}
class PageTitle extends React.Component<IPageTitle, any> {
public constructor(props, context) {
super(props, context);
}
public render() {
const { title, description, t } = this.props;
return (
<Helmet>
<title>{ t(stripHtml(title)) }</title>
{ description && <meta name="description" content={ t(description) } /> }
</Helmet>
);
}
}
export default withTranslation()(PageTitle);
......@@ -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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment