From 0d0d49873d55441c815c03180acd2313f66b4d21 Mon Sep 17 00:00:00 2001 From: michaelbenin Date: Tue, 3 Oct 2017 03:02:56 +0000 Subject: [PATCH] [FEATURE] - better real world example of dynamic content in HEAD --- gulpfile.babel.js/configs/config.js | 4 +- .../index/index_page_action_creators.js | 24 ++++ .../repo_detail_page_action_creators.js | 5 +- .../actions/index/async_index_page_actions.js | 19 +++ .../repo_detail/async_repo_detail_actions.js | 1 + .../async_repo_detail_page_actions.js | 4 +- src/redux/reducers/index.js | 4 + .../reducers/index/index_page_reducer.js | 26 ++++ src/redux/reducers/meta_reducer.js | 134 ++---------------- .../reducers/search/search_page_reducer.js | 6 +- src/redux/reducers/status_reducer.js | 10 ++ .../middleware/react_router_middleware.js | 3 +- src/utils/seo_util/base_seo_util.js | 8 +- src/utils/seo_util/index_seo_util.js | 30 ++++ src/utils/seo_util/repo_detail_seo_util.js | 30 ++++ src/utils/seo_util/search_seo_util.js | 30 ++++ .../pages/index_page/index_page_data_fetch.js | 18 +++ .../pages/index_page/index_page_route.js | 4 +- 18 files changed, 224 insertions(+), 136 deletions(-) create mode 100644 src/redux/action_creators/index/index_page_action_creators.js create mode 100644 src/redux/actions/index/async_index_page_actions.js create mode 100644 src/redux/reducers/index/index_page_reducer.js create mode 100644 src/utils/seo_util/index_seo_util.js create mode 100644 src/utils/seo_util/repo_detail_seo_util.js create mode 100644 src/utils/seo_util/search_seo_util.js create mode 100644 src/views/containers/pages/index_page/index_page_data_fetch.js diff --git a/gulpfile.babel.js/configs/config.js b/gulpfile.babel.js/configs/config.js index 2041bf3..6f8a344 100644 --- a/gulpfile.babel.js/configs/config.js +++ b/gulpfile.babel.js/configs/config.js @@ -25,9 +25,7 @@ const config = { env: { NODE_ENV: 'development' }, - nodeArgs: [ - // '--inspect' - ] + nodeArgs: ['--inspect'] }, test: { server: { diff --git a/src/redux/action_creators/index/index_page_action_creators.js b/src/redux/action_creators/index/index_page_action_creators.js new file mode 100644 index 0000000..4b737ad --- /dev/null +++ b/src/redux/action_creators/index/index_page_action_creators.js @@ -0,0 +1,24 @@ +export function indexPageLoading(state = {}) { + return { + type: 'INDEX_PAGE_LOADING', + isLoading: true, + state + }; +} + +export function indexPageLoaded(state = {}) { + return { + type: 'INDEX_PAGE_LOADED', + isLoading: false, + state + }; +} + +export function indexPageLoadError(state = {}, error) { + return { + type: 'INDEX_PAGE_ERROR', + isLoading: false, + state, + error + }; +} diff --git a/src/redux/action_creators/repo_detail/repo_detail_page_action_creators.js b/src/redux/action_creators/repo_detail/repo_detail_page_action_creators.js index 4c51814..810ba5d 100644 --- a/src/redux/action_creators/repo_detail/repo_detail_page_action_creators.js +++ b/src/redux/action_creators/repo_detail/repo_detail_page_action_creators.js @@ -8,11 +8,12 @@ export function repoDetailPageLoading(state = {}) { }; } -export function repoDetailPageLoaded(state = {}) { +export function repoDetailPageLoaded(state, data) { return { type: 'REPO_DETAIL_PAGE_LOADED', isLoading: false, - state + state, + data }; } diff --git a/src/redux/actions/index/async_index_page_actions.js b/src/redux/actions/index/async_index_page_actions.js new file mode 100644 index 0000000..b42c817 --- /dev/null +++ b/src/redux/actions/index/async_index_page_actions.js @@ -0,0 +1,19 @@ +import P from 'bluebird'; +import * as indexPageActions from '../../action_creators/index/index_page_action_creators'; +import notFoundActionCreator from '../../action_creators/not_found_status_action_creator'; +import log from '../../../services/logger_service'; + +export default function fetchIndexData(params, dispatch, state) { + dispatch(indexPageActions.indexPageLoading()); + return P.all([ + // This is a static page, but if you needed data + ]) + .then(function handleIndexPageDataLoaded() { + dispatch(indexPageActions.indexPageLoaded()); + }) + .catch(function handleUserError(err) { + log.error(err, 'Error in fetching repo detail page.'); + dispatch(notFoundActionCreator(500, 'ERROR_STATUS')); + dispatch(indexPageActions.indexPageLoadError(err, state)); + }); +} diff --git a/src/redux/actions/repo_detail/async_repo_detail_actions.js b/src/redux/actions/repo_detail/async_repo_detail_actions.js index 7c8984c..4459d4a 100644 --- a/src/redux/actions/repo_detail/async_repo_detail_actions.js +++ b/src/redux/actions/repo_detail/async_repo_detail_actions.js @@ -8,6 +8,7 @@ export default function fetchRepoDetail(params, dispatch, state) { return RepoDetailModel.fetch(params, state) .then(function handleRepoDetailData(repoData) { dispatch(repoDetailActions.repoDetailLoaded(repoData.data, state)); + return repoData.data; }) .catch(function handleUserError(err) { log.error(err, 'Error in fetching repo.'); diff --git a/src/redux/actions/repo_detail/async_repo_detail_page_actions.js b/src/redux/actions/repo_detail/async_repo_detail_page_actions.js index 999e006..9ba021c 100644 --- a/src/redux/actions/repo_detail/async_repo_detail_page_actions.js +++ b/src/redux/actions/repo_detail/async_repo_detail_page_actions.js @@ -8,8 +8,8 @@ import repoDetailAsyncAction from './async_repo_detail_actions'; export default function fetchRepoDetail(params, dispatch, state) { dispatch(repoDetailPageActions.repoDetailPageLoading()); return P.all([repoDetailAsyncAction(params, dispatch, state)]) - .then(function handleRepoDetailData() { - dispatch(repoDetailPageActions.repoDetailPageLoaded()); + .then(function handleRepoDetailData(actions) { + dispatch(repoDetailPageActions.repoDetailPageLoaded(state, actions)); }) .catch(function handleUserError(err) { log.error(err, 'Error in fetching repo detail page.'); diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js index db95630..2c7aed7 100644 --- a/src/redux/reducers/index.js +++ b/src/redux/reducers/index.js @@ -11,6 +11,8 @@ import repoDetailPage from './repo_detail/repo_detail_page_reducer'; import searchPage from './search/search_page_reducer'; import search from './search/search_results_reducer'; +import indexPage from './index/index_page_reducer'; + export default combineReducers({ routing, @@ -18,6 +20,8 @@ export default combineReducers({ meta, status, + indexPage, + repoDetailPage, repoDetail, diff --git a/src/redux/reducers/index/index_page_reducer.js b/src/redux/reducers/index/index_page_reducer.js new file mode 100644 index 0000000..6db05ea --- /dev/null +++ b/src/redux/reducers/index/index_page_reducer.js @@ -0,0 +1,26 @@ +export default function(state = {}, action) { + const typeMap = { + INDEX_PAGE_LOADING() { + return { + isLoading: true + }; + }, + INDEX_PAGE_LOADED() { + return { + isLoading: false + }; + }, + INDEX_PAGE_ERROR() { + return { + isLoading: false, + error: action.error + }; + } + }; + + if (typeMap[action.type]) { + return typeMap[action.type](); + } + + return state; +} diff --git a/src/redux/reducers/meta_reducer.js b/src/redux/reducers/meta_reducer.js index c760647..2fce28f 100644 --- a/src/redux/reducers/meta_reducer.js +++ b/src/redux/reducers/meta_reducer.js @@ -1,130 +1,24 @@ -import { get } from 'lodash'; +import repoDetailSeoUtil from '../../utils/seo_util/repo_detail_seo_util'; +import indexSeoUtil from '../../utils/seo_util/index_seo_util'; +import searchSeoUtil from '../../utils/seo_util/search_seo_util'; -function getMetaRepo(action) { - return { - title: `${get(action, 'repo.name')} - ${get(action, 'repo.description')}`, - meta: [ - { charSet: 'utf-8' }, - { - name: 'viewport', - content: - 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' - }, - { name: 'subject', content: '' }, - { name: 'description', content: `${get(action, 'repo.description')}` }, - { name: 'keywords', content: '' }, - { name: 'news_keywords', content: '' }, - { name: 'revised', content: '' }, - { name: 'distribution', content: '' }, - { name: 'topic', content: '' }, - { name: 'summary', content: '' }, - { name: 'category', content: '' }, - { name: 'coverage', content: '' }, - { name: 'robots', content: '' }, - { name: 'googlebot', content: '' }, - { name: 'application-name', content: '' }, - { name: 'language', content: '' }, - { name: 'rating', content: '' }, +const typeMap = { + INDEX_PAGE_LOADED(state, action) { + return indexSeoUtil(state, action); + }, - { name: 'twitter:card', content: '' }, - { name: 'twitter:url', content: '' }, - { name: 'twitter:title', content: '' }, - { - name: 'twitter:description', - content: `${get(action, 'repo.description')}` - }, - { - name: 'twitter:image', - content: `${get(action, 'repo.owner.avatar_url')}` - }, - { name: 'twitter:site', content: '' }, - { name: 'twitter:creator', content: '' }, + REPO_DETAIL_PAGE_LOADED({ state, data }) { + return repoDetailSeoUtil(state, data); + }, - { name: 'skype_toolbar', content: '' }, - { name: 'google-site-verification', content: '' }, - { name: 'p:domain_verify', content: '' }, - { name: 'yandex-verification', content: '' }, - { name: 'msvalidate.01', content: '' }, - { name: 'msapplication-TileColor', content: '' }, - { name: 'msapplication-TileImage', content: '' }, - { name: 'theme-color', content: '' }, - - { property: 'fb:app_id', content: '' }, - { property: 'fb:pages', content: '' }, - { property: 'fb:pages', content: '' }, - { property: 'og:url', content: '' }, - { property: 'og:title', content: '' }, - { property: 'og:image', content: '' }, - { - property: 'og:description', - content: `${get(action, 'repo.description')}` - }, - { property: 'og:type', content: '' }, - { property: 'og:site_name', content: '' }, - { property: 'og:locale', content: '' }, - { property: 'og:title', content: '' }, - { property: 'op:markup_version', content: '' }, - { property: 'fb:article_style', content: '' }, - - { itemProp: 'name', content: '' }, - { - itemProp: 'description', - content: `${get(action, 'repo.description')}` - }, - { itemProp: 'image', content: `${get(action, 'repo.owner.avatar_url')}` }, - - { httpEquiv: 'refresh', content: '' }, - { httpEquiv: 'x-ua-compatible', content: '' }, - { httpEquiv: 'cleartype', content: '' } - ], - link: [ - { rel: 'dns-prefetch', href: '' }, - { rel: 'shortcut-icon', href: '' }, - { rel: 'alternate', type: 'application/rss+xml', title: '', href: '' }, - { rel: 'canonical', href: '' }, - { rel: 'publisher', href: '' }, - { rel: 'image_src', href: `${get(action, 'repo.owner.avatar_url')}` }, - - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - { rel: 'apple-touch-icon', sizes: '', href: '' }, - - { rel: 'icon', type: 'image/png', sizes: '', href: '' }, - { rel: 'icon', type: 'image/png', sizes: '', href: '' }, - { rel: 'icon', type: 'image/png', sizes: '', href: '' }, - - { rel: 'manifest', href: '' } - ], - script: [ - { - type: 'application/ld+json', - innerHTML: "{ '@context': 'http://schema.org' }" - } - ], - noscript: [], - style: [] - }; -} + SEARCH_PAGE_LOADED(state, action) { + return searchSeoUtil(state, action); + } +}; export default function(state = {}, action) { const { type } = action; - const typeMap = { - REPO_DETAIL_LOADED(act) { - return getMetaRepo(act); - }, - - SEARCH_LOADED() { - return state; - } - }; - if (typeMap[type]) { return typeMap[type](action); } diff --git a/src/redux/reducers/search/search_page_reducer.js b/src/redux/reducers/search/search_page_reducer.js index aa5abf9..ae718f5 100644 --- a/src/redux/reducers/search/search_page_reducer.js +++ b/src/redux/reducers/search/search_page_reducer.js @@ -3,17 +3,17 @@ export default function(state = {}, action) { const typeMap = { - SEARCH_LOADING() { + SEARCH_PAGE_LOADING() { return { isLoading: true }; }, - SEARCH_LOADED() { + SEARCH_PAGE_LOADED() { return { isLoading: false }; }, - SEARCH_ERROR() { + SEARCH_PAGE_ERROR() { return { isLoading: false, error: action.error diff --git a/src/redux/reducers/status_reducer.js b/src/redux/reducers/status_reducer.js index 80945b4..7eff66e 100644 --- a/src/redux/reducers/status_reducer.js +++ b/src/redux/reducers/status_reducer.js @@ -1,3 +1,6 @@ +import { canUseDOM } from 'exenv'; +import extend from 'lodash/extend'; + export default function( state = { code: 200 @@ -15,5 +18,12 @@ export default function( code: 500 }; } + + if (/PAGE_LOADING/.test(action.type) && canUseDOM) { + return extend({}, state, { + code: 200 + }); + } + return state; } diff --git a/src/server/middleware/react_router_middleware.js b/src/server/middleware/react_router_middleware.js index 6438adb..0999171 100644 --- a/src/server/middleware/react_router_middleware.js +++ b/src/server/middleware/react_router_middleware.js @@ -30,7 +30,8 @@ export default (req, res) => { staticUrl, apiUrl, initialPageLoad: true, - featureFlags + featureFlags, + navHistory: [`${req.protocol}://${req.hostname}${req.originalUrl}`] } }); diff --git a/src/utils/seo_util/base_seo_util.js b/src/utils/seo_util/base_seo_util.js index e4a3f05..7209d92 100644 --- a/src/utils/seo_util/base_seo_util.js +++ b/src/utils/seo_util/base_seo_util.js @@ -2,7 +2,7 @@ import get from 'lodash/get'; export default function(data = {}) { const { siteConf, title, type, publisher = true, extend = [] } = data; - const siteHumanReadable = get(siteConf, '[og:site_name]') || ''; + const siteHumanReadable = get(siteConf, '[og:site_name]', ''); const description = data.description || ''; const url = data.url || ''; const siteSlug = data.slug || ''; @@ -20,7 +20,7 @@ export default function(data = {}) { }, { property: 'fb:pages', - content: `${get(data, 'facebook.fanpageId')}` + content: get(data, 'facebook.fanpageId') }, { name: 'viewport', @@ -61,11 +61,11 @@ export default function(data = {}) { }, { property: 'fb:app_id', - content: `${get(data, 'appId')}` + content: get(data, 'appId') }, { property: 'fb:admins', - content: `${get(data, 'appAdminId')}` + content: get(data, 'appAdminId') }, { name: 'twitter:card', diff --git a/src/utils/seo_util/index_seo_util.js b/src/utils/seo_util/index_seo_util.js new file mode 100644 index 0000000..ff45c5e --- /dev/null +++ b/src/utils/seo_util/index_seo_util.js @@ -0,0 +1,30 @@ +import baseSeoUtil from './base_seo_util'; + +// eslint-disable-next-line no-unused-vars +export default function(state, action) { + const data = baseSeoUtil({ + site: '', + siteConf: {}, + title: '', + url: '', + description: '', + image: '', + image_width: '', + image_height: '', + articleSection: '', + extend: [ + { + meta: [ + /* + { + property: 'article:published_time', + content: articleDate + } + */ + ] + } + ] + }); + + return data; +} diff --git a/src/utils/seo_util/repo_detail_seo_util.js b/src/utils/seo_util/repo_detail_seo_util.js new file mode 100644 index 0000000..5df7ec8 --- /dev/null +++ b/src/utils/seo_util/repo_detail_seo_util.js @@ -0,0 +1,30 @@ +import get from 'lodash/get'; +import baseSeoUtil from './base_seo_util'; + +export default function(state, data) { + return baseSeoUtil({ + site: 'react-ssr-spa', + siteConf: { + '[og:site_name]': 'react-ssr-spa' + }, + facebook: { + fanpageId: '' + }, + title: get(data, '[0].name'), + url: get(state, 'config.navHistory[0]'), + description: get(data, '[0].description'), + image: get(data, '[0].owner.avatar_url'), + articleSection: get(data, '[0].language'), + appId: '', + extend: [ + { + meta: [ + { + property: 'article:published_time', + content: get(data, '[0].created_at') + } + ] + } + ] + }); +} diff --git a/src/utils/seo_util/search_seo_util.js b/src/utils/seo_util/search_seo_util.js new file mode 100644 index 0000000..ff45c5e --- /dev/null +++ b/src/utils/seo_util/search_seo_util.js @@ -0,0 +1,30 @@ +import baseSeoUtil from './base_seo_util'; + +// eslint-disable-next-line no-unused-vars +export default function(state, action) { + const data = baseSeoUtil({ + site: '', + siteConf: {}, + title: '', + url: '', + description: '', + image: '', + image_width: '', + image_height: '', + articleSection: '', + extend: [ + { + meta: [ + /* + { + property: 'article:published_time', + content: articleDate + } + */ + ] + } + ] + }); + + return data; +} diff --git a/src/views/containers/pages/index_page/index_page_data_fetch.js b/src/views/containers/pages/index_page/index_page_data_fetch.js new file mode 100644 index 0000000..93d077b --- /dev/null +++ b/src/views/containers/pages/index_page/index_page_data_fetch.js @@ -0,0 +1,18 @@ +import { canUseDOM } from 'exenv'; +import asyncIndexPageAction from '../../../../redux/actions/index/async_index_page_actions'; +import log from '../../../../services/logger_service'; + +export default function fetchIndexData(match, dispatch, state) { + if (canUseDOM) { + if (state.config.initialPageLoad === true) { + return false; + } + } + return asyncIndexPageAction( + match.params, + dispatch, + state + ).catch(function handleError(err) { + log.error(err); + }); +} diff --git a/src/views/containers/pages/index_page/index_page_route.js b/src/views/containers/pages/index_page/index_page_route.js index 7e5e670..205ba75 100644 --- a/src/views/containers/pages/index_page/index_page_route.js +++ b/src/views/containers/pages/index_page/index_page_route.js @@ -1,8 +1,10 @@ import IndexPage from './index_page'; +import indexPageLoadData from './index_page_data_fetch'; export default { path: '/', component: IndexPage, exact: true, - strict: true + strict: true, + loadData: indexPageLoadData };