Introduced Server Side Rendering for Cloud Pages based on React
This commit is contained in:
1 parent
569b849457
commit
1d6772eef3
17 files changed
+456
-61
No files matched your search
+13
-4
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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>© 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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': 'Текущий контроль',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,4 +36,13 @@ footer {
|
||||
box-sizing: padding-box;
|
||||
}
|
||||
}
|
||||
|
||||
&.footer--centered {
|
||||
.copyright {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>© 2012—2017 Шлиссельбургская детская художественная школа</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user