Introduced Server Side Rendering for Cloud Pages based on React

This commit is contained in:
tmshv committed 2017-05-20 23:24:18 +03:00
1 parent 569b849457
commit 1d6772eef3
17 files changed
+456 -61

No files matched your search

+13 -4
View File
@@ -233,16 +233,25 @@ app.config(($locationProvider, $routeProvider) => {
})
$routeProvider.otherwise({
templateUrl: '/404.html'
template: '',
controller: function($window) {
console.log('otherwise')
$window.location.reload()
}
})
})
app.run(($location, $rootScope, $http) => {
app.run(($window, $location, $rootScope, $http) => {
$rootScope.$on('$routeChangeSuccess', (event, current) => {
const url = location.href
const title = current['$$route'].title
if (title) $rootScope.title = title
try {
const title = current['$$route'].title
if (title) $rootScope.title = title
} catch (e) {
}
setTimeout(() => {
try {
+31
View File
@@ -0,0 +1,31 @@
const React = require('react')
const Menu = require('./Menu')
const Footer = require('./Footer')
const App = ({children, menu}) => (
<div className="body-wrapper theme-default">
<header>
<div className="navigation">
<div className="navigation__body">
<Menu items={menu.items}/>
</div>
</div>
</header>
<main className="body-wrapper__content">
<section className="content content_thin content--padding-top--menu">
<div className="cloud-page__body">
{children}
</div>
</section>
</main>
<Footer showAuthor={false}
address="г. Шлиссельбург ул. 18 января д. 3"
telephone="+7 (81362) 76-312"
email="hudozka@gmail.com"
/>
</div>
)
module.exports = App
+31
View File
@@ -0,0 +1,31 @@
const React = require('react')
const mailto = email => `mailto:${email}`
const Footer = ({children, address, telephone, email, showAuthor}) => (
<footer>
<div className="contacts">
<div>
<h4>Контактная информация</h4>
<p>Адрес: {address}</p>
<p>Телефон: {telephone}</p>
<p>Почта: <a href={mailto(email)}>{email}</a></p>
<p>Вконтакте: <a href="https://vk.com/shlisselburghudozka">shlisselburghudozka</a></p>
<p>Инстаграм: <a href="https://www.instagram.com/hudozka/">hudozka</a></p>
</div>
</div>
<div className="copyright">
<span>©&nbsp;2012—2017 Шлиссельбургская детская художественная школа</span>
{!showAuthor ? null : (
<span>Разработка и поддержка сайта —
<a href="http://tmshv.ru?utm_source=artshburg&utm_medium=developer&utm_campaign=tmshv_ru">
www.tmshv.ru
</a>
</span>
)}
</div>
</footer>
)
module.exports = Footer
+39
View File
@@ -0,0 +1,39 @@
const React = require('react')
const hasSubmenu = item => !!item.items
const doDrawSubmenu = item => false //item.active && hasSubmenu(item)
const selectedClassName = active => active
? 'selected'
: ''
const MenuItem = ({active, text, url}) => (active
? <span>{text}</span>
: <a href={url}>{text}</a>
)
const Menu = ({items}) => {
const content = items.map((item, index) => (
<li key={index} className={`${item.color} ${selectedClassName(item.active)}`}>
<MenuItem {...item}/>
{!doDrawSubmenu(item) ? null :
<Submenu key={index} items={item.items}/>
}
</li>
))
return (
<menu className="main-menu">{content}</menu>
)
}
const Submenu = ({items}) => (
<menu className="main-menu__submenu">
{items.map((item, index) => (
<li key={index} className={`${item.color} ${selectedClassName(item.active)}`}>
<MenuItem {...item}/>
</li>
))}
</menu>
)
module.exports = Menu
+36
View File
@@ -0,0 +1,36 @@
function compose(...fns) {
return value => [...fns]
.reverse()
.reduce((acc, fn) => fn(acc), value)
}
/**
* AND operator for list of values
*
* [true, true, true, ...] -> true
* [false, true, true, ...] -> false
*
* @param list
* @return {boolean}
*/
function all(list) {
return list.reduce((t, i) => t && i, true)
}
/**
* OR operator for list of values
*
* [true, true, true, ...] -> true
* [false, true, true, ...] -> true
* [false, false, false, ...] -> false
*
* @param list
* @return {boolean}
*/
function any(list) {
return list.reduce((t, i) => t || i, false)
}
exports.compose = compose
exports.all = all
exports.any = any
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
const App = require('../components/App')
function renderApp({menu, page}) {
const app = (
<App menu={menu}>
<div dangerouslySetInnerHTML={{__html: page.data}}/>
</App>
)
return renderToStaticMarkup(app)
}
exports.renderApp = renderApp
+14
View File
@@ -0,0 +1,14 @@
function getPathWithNoTrailingSlash(path) {
return path.charAt(path.length - 1) === '/'
? path.substr(0, path.length - 1)
: path
}
function isEqualPath(a, b, ignoreTrailingSlash = false) {
return ignoreTrailingSlash
? getPathWithNoTrailingSlash(a) === getPathWithNoTrailingSlash(b)
: a === b
}
exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash
exports.isEqualPath = isEqualPath
+18
View File
@@ -0,0 +1,18 @@
const {readFile} = require('async-file')
const handlebars = require('handlebars')
function notFound(templateFile) {
return async (ctx, next) => {
await next()
if (ctx.status === 404) {
const source = await readFile(templateFile, 'utf-8')
const template = handlebars.compile(source)
ctx.type = 'text/html'
ctx.body = template()
}
}
}
exports.notFound = notFound
+42 -9
View File
@@ -1,8 +1,11 @@
export default [
{
'text': 'Школа',
'url': '/',
'color': 'blue'
url: '/',
text: 'Школа',
color: 'blue',
items: [
{url: '/summer-school/2017', text: 'Летние каникулы 2017'},
],
},
{
'text': 'События',
@@ -20,9 +23,24 @@ export default [
'color': 'yellow'
},
{
'text': 'Предметы',
'url': '/courses',
'color': 'magenta'
url: '/courses/',
text: 'Предметы',
color: 'magenta',
items: [
{url: 'ceramics', text: 'Керамика'},
{url: 'batik', text: 'Живопись'},
{url: 'easel-composition', text: 'Живопись'},
{url: 'cg', text: 'Живопись'},
{url: 'scale-modeling', text: 'Живопись'},
{url: 'drawing', text: 'Живопись'},
{url: 'costume-composition', text: 'Живопись'},
{url: 'felting', text: 'Живопись'},
{url: 'art-history', text: 'Живопись'},
{url: 'perishki', text: 'Живопись'},
{url: 'matreshki', text: 'Живопись'},
{url: 'listochki', text: 'Живопись'},
]
},
{
'text': 'Коллектив',
@@ -40,9 +58,24 @@ export default [
'color': 'yellow'
},
{
'text': 'Сведения об образовательной организации',
'url': '/info',
'color': 'gray'
url: '/info/',
text: 'Сведения об образовательной организации',
color: 'gray',
items: [
{url: 'main', text: 'Основные сведения'},
{url: 'structure', text: 'Структура и органы управления образовательной организацией'},
{url: 'education', text: 'Образование'},
{url: 'standards', text: 'Образовательные стандарты'},
{url: '/collective', text: 'Руководство. Педагогический (научно-педагогический) состав'},
{url: 'support', text: 'Материально-техническое обеспечение образовательного процесса'},
{url: 'grants', text: 'Стипендии и иные виды материальной поддержки'},
{url: 'paid-services', text: 'Платные образовательные услуги'},
{url: 'finances', text: 'Финансово-хозяйственная деятельность'},
{url: 'vacancy', text: 'Вакантные места для приема'},
{url: 'tools', text: 'Список рекомендованных материалов'},
{url: 'history', text: 'История школы'},
]
},
{
'text': 'Текущий контроль',
-26
View File
@@ -1,26 +0,0 @@
import {index, accepts} from './';
export default function () {
return async(ctx) => {
if (ctx.status != 404) return;
ctx.status = 404;
ctx.body = 'lol found';
await accepts({
'text/html': index(),
'text/plain': async ctx => {
ctx.type = 'text/plain';
ctx.body = 'Page not found';
},
'application/json': async ctx => {
ctx.type = 'application/json';
ctx.body = {
error: 404,
message: 'Not Found'
};
}
})(ctx);
};
};
+1 -4
View File
@@ -11,7 +11,6 @@ import schedule from './schedule'
import {getPageUrls} from '../core/pages'
import news from './news'
import gallery from './gallery'
import error404 from './404'
import documents from './documents'
import teachers from './teachers'
import events from './events'
@@ -32,7 +31,6 @@ export function routes(store) {
events(),
articles(),
pages(),
error404()
])
}
@@ -96,6 +94,5 @@ export function json(fn) {
}
async function getInitialData() {
const pages = await getPageUrls()
return {pages: pages}
return {pages: []}
}
+64 -17
View File
@@ -1,24 +1,71 @@
import route from 'koa-route'
import {c} from '../core/db'
import {accepts, index} from './'
const {readFile} = require('async-file')
const handlebars = require('handlebars')
async function exists(ctx) {
const page = await getPage(ctx.path)
return Boolean(page)
}
const {get} = require('koa-route')
const {c} = require('../core/db')
const url = require('url')
const renderApp = require('../lib/component').renderApp
const isEqualPath = require('../lib/url').isEqualPath
const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash
const {compose, any} = require('../lib/common')
const menuModel = require('../models/menu').default
async function getPage(path) {
return await c('pages').findOne({url: path})
}
export default function () {
return route.get('*', accepts({
'text/html': index(exists),
'text/plain': index(exists),
'application/json': async(ctx) => {
const page = await getPage(ctx.path)
if (!page) ctx.status = 404
else ctx.body = page
}
function isActive(path, menuItem) {
return any(flatMenuItem(menuItem)
.map(i => isEqualPath(path, i.url, true))
)
}
function flatMenuItem(item) {
const rootUrl = item.url
const flatUrls = items => {
return items.map(item => ({
...item,
url: url.resolve(rootUrl, item.url)
}))
}
return !item.items
? [item]
: item.items.reduce((acc, i) => [...acc, ...compose(flatUrls, flatMenuItem)(i)], [item])
}
/**
* Takes plain menu model and sample path and defines is an item of menu should be active
*
* @param path
* @param menu
* @return {{items}}
*/
function buildMenu(path, menu) {
const items = menu.map(item => ({
...item,
active: isActive(path, item)
}))
};
return {items}
}
module.exports = function () {
return get('*', async ctx => {
const path = getPathWithNoTrailingSlash(ctx.path)
const page = await getPage(path)
if (page) {
const menu = buildMenu(path, menuModel)
const content = renderApp({menu, page})
const source = await readFile('src/views/main.hbs', 'utf-8')
const template = handlebars.compile(source)
ctx.type = 'text/html'
ctx.body = template({content})
} else {
ctx.status = 404
}
})
}
+4
View File
@@ -19,6 +19,8 @@ import {redirect} from './routes/redirect'
import {services as serviceKeys, authChecker} from './core/service'
import handlebars from 'handlebars'
const error = require('./middlewares/error')
const dirPublic = path.join(__dirname, '../public')
const dirTemplates = path.join(__dirname, 'templates')
@@ -47,6 +49,8 @@ export default function (store) {
app.use($(redirect(redirectionTable)))
app.use($(helmet()))
app.use(queryObject())
app.use(error.notFound('src/views/404.hbs'))
app.use(routes(store))
return Server(app.callback())
+20 -1
View File
@@ -7,11 +7,30 @@
list-style: none;
li {
.main-menu__submenu {
background-color: rgba(255, 255, 255, 0.95);
display: block;
position: absolute;
top: 100%;
left: -1em;
padding: 0 0.5em 0.5em;
& > li {
display: block;
padding: 0.3em 0.2em 0.5em;
margin: 0 0.5em;
}
}
& > li {
display: inline-block;
padding: 0.3em 0.2em 0.5em;
margin: 0 0.5em;
position: relative;
&:first-child {
margin-left: 1em;
}
+9
View File
@@ -36,4 +36,13 @@ footer {
box-sizing: padding-box;
}
}
&.footer--centered {
.copyright {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta name="fragment" content="!">
<meta name="google-site-verification" content="8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg">
<meta name="google-site-verification" content="A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw"/>
<meta name='yandex-verification' content='643c7dac144b9dac'/>
<meta name="msvalidate.01" content="E878301FE3E3F12D233A6B1156889601"/>
<meta name="viewport" content="width=device-width, maximum-scale=1.0"/>
<meta name="referrer" content="unsafe-url">
<base href="/">
<title>По вашему запросу ничего не найдено — Шлиссельбургская ДХШ</title>
<link rel="icon" type="image/png" href="//static.shlisselburg.org/art/graphics/favicon-32.png"/>
<link rel="apple-touch-icon" sizes="57x57" href="//static.shlisselburg.org/art/graphics/favicon-57.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="//static.shlisselburg.org/art/graphics/favicon-72.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="//static.shlisselburg.org/art/graphics/favicon-114.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="//static.shlisselburg.org/art/graphics/favicon-144.png"/>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/fonts/PTSans/stylesheet.css">
<link rel="stylesheet" href="/fonts/PTSerif/stylesheet.css">
</head>
<body>
<div class="body-wrapper">
<header>
</header>
<main class="body-wrapper__content">
<section class="content content_wide content--padding-top--menu">
<div class="cloud-page__body">
<div class="content content_wide content--padding-top--menu">
<div class="error-404">
<div>
<h1>Страница не найдена.</h1>
<img src="http://static.shlisselburg.org/art/graphics/skull.jpg" alt=""/>
</div>
<div><a href="/">На главную</a></div>
</div>
</div>
</div>
</section>
</main>
<footer class="footer--centered">
<div class="copyright">
<span>©&nbsp;2012—2017 Шлиссельбургская детская художественная школа</span>
</div>
</footer>
</div>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta name="fragment" content="!">
<meta name="google-site-verification" content="8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg">
<meta name="google-site-verification" content="A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw"/>
<meta name='yandex-verification' content='643c7dac144b9dac'/>
<meta name="msvalidate.01" content="E878301FE3E3F12D233A6B1156889601"/>
<meta name="viewport" content="width=device-width, maximum-scale=1.0"/>
<meta name="referrer" content="unsafe-url">
<base href="/">
<title>Шлиссельбургская Детская Художественная Школа</title>
<link rel="icon" type="image/png" href="//static.shlisselburg.org/art/graphics/favicon-32.png"/>
<link rel="apple-touch-icon" sizes="57x57" href="//static.shlisselburg.org/art/graphics/favicon-57.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="//static.shlisselburg.org/art/graphics/favicon-72.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="//static.shlisselburg.org/art/graphics/favicon-114.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="//static.shlisselburg.org/art/graphics/favicon-144.png"/>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/fonts/PTSans/stylesheet.css">
<link rel="stylesheet" href="/fonts/PTSerif/stylesheet.css">
<script>
if (window.location.host.indexOf('localhost') === -1) {
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
// ga('create', 'UA-60209057-1', 'auto');
//ga('send', 'pageview');
}
</script>
</head>
<body>
{{{content}}}
<script>
function initDisqus() {
const script = document.createElement('script')
script.src = '//hudozka.disqus.com/embed.js';
script.setAttribute('data-timestamp', Date.now());
(document.head || document.body).appendChild(script);
}
const doInitDisqus = false
if (doInitDisqus) initDisqus()
</script>
</body>
</html>