Merge pull request #5 from LordMathis/develop-server

Server side rendering
This commit is contained in:
Matúš Námešný 2019-03-27 19:15:16 +01:00 committed by GitHub
commit 3ed99bdfbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2443 additions and 1070 deletions

View File

@ -1,17 +0,0 @@
{
"presets":[
"es2015", "react"
],
"env": {
"development": {
"presets": ["es2015", "react", "stage-0"],
"plugins": ["transform-runtime"],
"presets": ["react-hmre"]
}
},
"env": {
"build": {
"presets": ["es2015", "react", "stage-0"]
}
}
}

34
.editorconfig Normal file
View File

@ -0,0 +1,34 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

23
.eslintrc.json Normal file
View File

@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"standard",
"plugin:react/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "babel-eslint",
"plugins": [
"babel",
"react"
],
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
}
}

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
node_modules node_modules
dist build
public public
*.log *.log
content content

38
babel.config.js Normal file
View File

@ -0,0 +1,38 @@
module.exports = function (api) {
const presets = [
'@babel/preset-env',
'@babel/react'
]
const plugins = [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-template-literals',
'@babel/plugin-proposal-class-properties'
]
if (api.env() === 'development') {
plugins.push([
'css-modules-transform', {
'generateScopedName': '[name]__[local]___[hash:base64:5]',
'preprocessCss': processSass,
'extensions': ['.css', '.scss']
}
])
}
return {
presets,
plugins
}
}
var sass = require('node-sass')
function processSass (data, filename) {
var result
result = sass.renderSync({
data: data,
file: filename
}).css
return result.toString('utf8')
}

View File

@ -4,13 +4,13 @@
"name": "Matúš Námešný", "name": "Matúš Námešný",
"email": "matus@namesny.com", "email": "matus@namesny.com",
"social": { "social": {
"twitter": "https://twitter.com/matus_n",
"github": "https://github.com/LordMathis", "github": "https://github.com/LordMathis",
"codepen": "https://codepen.io/LordMathis/", "codepen": "https://codepen.io/LordMathis/",
"linkedin": "https://www.linkedin.com/in/mat%C3%BA%C5%A1-n%C3%A1me%C5%A1n%C3%BD-3903b6128/" "linkedin": "https://www.linkedin.com/in/mat%C3%BA%C5%A1-n%C3%A1me%C5%A1n%C3%BD-3903b6128/"
}, },
"contentPath": "./content", "contentPath": "./content",
"renderPath": "./renders", "renderPath": "./renders",
"dataPath": "./src/utils/data.json",
"files": [ "files": [
"about.md", "resume.md" "about.md", "resume.md"
] ]

View File

@ -1,12 +1,14 @@
{ {
"name": "portfolio", "name": "portfolio",
"version": "1.0.0", "version": "2.0.0",
"description": "portfolio", "description": "portfolio",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "NODE_ENV=production webpack -p --progress --config webpack.prod.config.js", "build": "npm run build-client && npm run build-server",
"start": "NODE_ENV=production node ./src/server.js", "build-client": "NODE_ENV=production webpack --config webpack.client.js -p --progress",
"dev": "NODE_ENV=development babel-node ./src/server.js --presets es2015,stage-2 ./srcserver.js" "build-server": "NODE_ENV=production webpack --config webpack.server.js -p --progress",
"start": "NODE_ENV=production node ./build/server.js",
"start-dev": "NODE_ENV=development babel-node ./src/server.js"
}, },
"keywords": [ "keywords": [
"porfolio", "porfolio",
@ -16,34 +18,45 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"async": "^2.5.0", "async": "^2.5.0",
"axios": "^0.17.0",
"express": "^4.13.4", "express": "^4.13.4",
"express-static-gzip": "^1.1.3",
"front-matter": "^2.2.0", "front-matter": "^2.2.0",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
"markdown-it": "^8.4.0", "markdown-it": "^8.4.2",
"moment": "^2.19.1", "moment": "^2.24.0",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"react": "^15.0.1", "prop-types": "^15.7.2",
"react-dom": "^15.0.1", "react": "^16.7.0",
"react-router-dom": "^4.1.1" "react-dom": "^16.7.0",
"react-router-dom": "^4.1.1",
"serialize-javascript": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "@babel/core": "^7.2.2",
"babel-core": "^6.7.6", "@babel/node": "^7.2.2",
"babel-jest": "*", "@babel/plugin-proposal-class-properties": "^7.3.3",
"babel-loader": "^7.1.4", "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
"babel-plugin-transform-runtime": "^6.7.5", "@babel/plugin-transform-runtime": "^7.2.0",
"babel-polyfill": "^6.26.0", "@babel/plugin-transform-template-literals": "^7.2.0",
"babel-preset-es2015": "^6.6.0", "@babel/preset-env": "^7.2.3",
"babel-preset-react": "^6.5.0", "@babel/preset-react": "^7.0.0",
"babel-preset-react-hmre": "^1.1.1", "@babel/runtime": "^7.2.0",
"babel-preset-stage-0": "^6.5.0", "babel-eslint": "^10.0.1",
"babel-register": "^6.7.2", "babel-loader": "^8.0.5",
"babel-runtime": "^6.26.0", "babel-plugin-css-modules-transform": "^1.6.2",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11", "compression-webpack-plugin": "^1.1.11",
"create-file-webpack": "^1.0.2",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"css-modules-require-hook": "^4.0.6", "css-modules-require-hook": "^4.2.3",
"eslint": "^5.14.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-standard": "^4.0.0",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"mini-css-extract-plugin": "^0.4.0", "mini-css-extract-plugin": "^0.4.0",
"optimize-css-assets-webpack-plugin": "^4.0.2", "optimize-css-assets-webpack-plugin": "^4.0.2",
@ -55,6 +68,7 @@
"webpack-cli": "^2.1.2", "webpack-cli": "^2.1.2",
"webpack-dev-middleware": "^3.1.3", "webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.18.0", "webpack-hot-middleware": "^2.18.0",
"webpack-manifest-plugin": "^2.0.3" "webpack-manifest-plugin": "^2.0.4",
"webpack-node-externals": "^1.7.2"
} }
} }

View File

@ -1 +1 @@
module.exports = {}; module.exports = {}

View File

@ -1,17 +1,11 @@
import React from 'react'; import React from 'react'
import {render} from 'react-dom'; import { hydrate } from 'react-dom'
import {BrowserRouter as Router} from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom'
import {App} from './components'; import { App } from './components'
const AppClient = () => ( hydrate(
<Router> <Router>
<App /> <App data={window.__INITIAL_DATA__}/>
</Router> </Router>,
document.getElementById('root')
) )
window.onload = () => {
render(
<AppClient />,
document.getElementById('root')
);
};

View File

@ -1,24 +1,32 @@
import React, {Component} from 'react'; import PropTypes from 'prop-types'
import {Spinner, Header} from '.'; import React, { Component } from 'react'
import '../static/stylesheets/globals.scss'; import { Spinner, Header } from '.'
import styles from './About.scss'; import '../static/stylesheets/globals.scss'
import contentStyle from '../static/stylesheets/content.scss'; import contentStyle from '../static/stylesheets/content.scss'
import MarkdownIt from 'markdown-it'
export default class About extends Component { export default class About extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
about: PropTypes.string.isRequired
}
render () { render () {
const md = MarkdownIt()
const result = md.render(this.props.about)
if (this.props.isLoading) { if (this.props.isLoading) {
return ( return (
<div className={contentStyle.contentWrapper} id="about"> <div className={contentStyle.contentWrapper} id="about">
<Spinner/> <Spinner/>
</div> </div>
); )
} }
return ( return (
<div className={contentStyle.contentWrapper} id="about"> <div className={contentStyle.contentWrapper} id="about">
<Header header={"About Me"} /> <Header header={'About Me'} />
<div className={contentStyle.content} dangerouslySetInnerHTML={{__html: this.props.about.body}}> <div className={contentStyle.content} dangerouslySetInnerHTML={{ __html: result }}>
</div> </div>
</div> </div>
) )

View File

@ -1,16 +1,26 @@
import React from 'react'; import { NotFoundWrapper } from '.'
import { Route, Switch } from 'react-router-dom'; import React, { Component } from 'react'
import { Home, NotFoundWrapper } from '.'; import routes from '../utils/routes'
import { MainContainer, PostContainer } from '../containers'; import { Route, Switch } from 'react-router-dom'
export const App = () => ( export default class App extends Component {
<div> render () {
<Switch> return (
<Route exact path="/" component={MainContainer} /> <div>
<Route path="/post/:postname" component={PostContainer} /> <Switch>
<Route component={NotFoundWrapper} /> {routes.map(({ path, exact, component: C, ...rest }) => (
</Switch> <Route
</div> key={path}
); path={path}
exact={exact}
export default App; render={(props) => (
<C {...props} {...rest} />
)}
/>
))}
<Route render={(props) => <NotFoundWrapper {...props} />} />
</Switch>
</div>
)
}
}

View File

@ -1,21 +1,29 @@
import React, {Component} from 'react'; import PropTypes from 'prop-types'
import {Spinner, Header} from '.'; import React, { Component } from 'react'
import '../static/stylesheets/globals.scss'; import { Spinner, Header } from '.'
import styles from './Blog.scss'; import '../static/stylesheets/globals.scss'
import contentStyle from '../static/stylesheets/content.scss'; import styles from './Blog.scss'
import contentStyle from '../static/stylesheets/content.scss'
export default class Blog extends Component { export default class Blog extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
posts: PropTypes.arrayOf(PropTypes.object).isRequired
}
render() { render () {
if (this.props.isLoading) { if (this.props.isLoading) {
return ( return (
<div className={contentStyle.contentWrapper} id="blog"> <div className={contentStyle.contentWrapper} id="blog">
<Spinner/> <Spinner/>
</div> </div>
); )
} }
let posts = this.props.posts.map((post) => let posts = this.props.posts.sort((a, b) => {
return new Date(b.published) - new Date(a.published)
})
let postsHTML = posts.map((post) =>
<tr className={styles.postListItem} key={post.title}> <tr className={styles.postListItem} key={post.title}>
<td> <td>
<span className={styles.postDate}>{post.published}</span> <span className={styles.postDate}>{post.published}</span>
@ -28,17 +36,17 @@ export default class Blog extends Component {
return ( return (
<div className={contentStyle.contentWrapper} id="blog"> <div className={contentStyle.contentWrapper} id="blog">
<Header header={"Blog"} /> <Header header={'Blog'} />
<div className={contentStyle.content}> <div className={contentStyle.content}>
<table> <table>
<tbody className={styles.postsWrapper}> <tbody className={styles.postsWrapper}>
{posts} {postsHTML}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
); )
} }
}; };

View File

@ -1,14 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react'
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom'
import config from '../../config.json'; import config from '../../config.json'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import styles from './Home.scss'; import styles from './Home.scss'
export default class Home extends Component { export default class Home extends Component {
render () {
render() { let key = 0
let key = 0; const objKeys = Object.keys(config.social)
const objKeys = Object.keys(config.social);
const socialLinks = objKeys.map((val) => { const socialLinks = objKeys.map((val) => {
const link = ( const link = (
@ -16,17 +15,17 @@ export default class Home extends Component {
<i className={`fa fa-${val} fa-3x`} aria-hidden="true" /> <i className={`fa fa-${val} fa-3x`} aria-hidden="true" />
<span className="sr-only">{val}</span> <span className="sr-only">{val}</span>
</a> </a>
); )
key += 1; key += 1
return link; return link
}); })
socialLinks.push( socialLinks.push(
<a key={key} href={`mailto:${config.email}`}> <a key={key} href={`mailto:${config.email}`}>
<i className="fa fa-envelope-o fa-3x" aria-hidden="true" /> <i className="fa fa-envelope-o fa-3x" aria-hidden="true" />
<span className="sr-only">e-mail</span> <span className="sr-only">e-mail</span>
</a>, </a>
); )
return ( return (
<div id={styles.coverPage} className={styles.coverPageFull}> <div id={styles.coverPage} className={styles.coverPageFull}>
@ -53,6 +52,6 @@ export default class Home extends Component {
</div> </div>
</div> </div>
</div> </div>
); )
} }
} }

View File

@ -1,14 +1,12 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import config from '../../config.json'; import config from '../../config.json'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import styles from './Navbar.scss'; import styles from './Navbar.scss'
export default class Navbar extends Component { export default class Navbar extends Component {
render () { render () {
let key = 0
let key = 0; const objKeys = Object.keys(config.social)
const objKeys = Object.keys(config.social);
const socialLinks = objKeys.map((val) => { const socialLinks = objKeys.map((val) => {
const link = ( const link = (
@ -16,17 +14,18 @@ export default class Navbar extends Component {
<i className={`fa fa-${val}`} aria-hidden="true" /> <i className={`fa fa-${val}`} aria-hidden="true" />
<span className="sr-only">{val}</span> <span className="sr-only">{val}</span>
</a> </a>
); )
key += 1;
return link; key += 1
}); return link
})
socialLinks.push( socialLinks.push(
<a key={key} href={`mailto:${config.email}`}> <a key={key} href={`mailto:${config.email}`}>
<i className="fa fa-envelope-o" aria-hidden="true" /> <i className="fa fa-envelope-o" aria-hidden="true" />
<span className="sr-only">e-mail</span> <span className="sr-only">e-mail</span>
</a>, </a>
); )
return ( return (
<div className={styles.navbar}> <div className={styles.navbar}>

View File

@ -1,21 +1,21 @@
import React from 'react'; import React from 'react'
import {Navbar, Header} from '.'; import { Navbar, Header } from '.'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import contentStyle from '../static/stylesheets/content.scss'; import contentStyle from '../static/stylesheets/content.scss'
export const NotFoundPage = (props) => { export const NotFoundPage = (props) => {
return ( return (
<div> <div>
<Navbar /> <Navbar />
<div className={contentStyle.contentWrapper}> <div className={contentStyle.contentWrapper}>
<Header header={"Uhm... WHAT?"} /> <Header header={'Uhm... WHAT?'} />
<div className={contentStyle.content}> <div className={contentStyle.content}>
<p>Looks like you&apos;re lost</p> <p>Looks like you&apos;re lost</p>
<p>404 Page not found</p> <p>404 Page not found</p>
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default NotFoundPage; export default NotFoundPage

View File

@ -1,10 +1,8 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import {Wrapper, NotFoundPage} from '.'; import { Wrapper, NotFoundPage } from '.'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import styles from './Wrapper.scss';
export default class NotFoundWrapper extends Component { export default class NotFoundWrapper extends Component {
render () { render () {
return ( return (
<Wrapper> <Wrapper>

View File

@ -1,30 +1,44 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import {Spinner, Header, Navbar} from '.'; import PropTypes from 'prop-types'
import '../static/stylesheets/globals.scss'; import { Spinner, Header, Navbar } from '.'
import contentStyle from '../static/stylesheets/content.scss'; import '../static/stylesheets/globals.scss'
import styles from './Post.scss'; import contentStyle from '../static/stylesheets/content.scss'
import styles from './Post.scss'
import MarkdownIt from 'markdown-it'
import fm from 'front-matter'
import moment from 'moment'
export default class Post extends Component { export default class Post extends Component {
render() { static propTypes = {
isLoading: PropTypes.bool.isRequired,
post: PropTypes.object.isRequired
}
render () {
const md = MarkdownIt()
const content = fm(this.props.post)
const title = content.attributes.title
const date = moment(content.attributes.date, 'YYYY-MM-DD')
const body = md.render(content.body)
if (this.props.isLoading) { if (this.props.isLoading) {
return ( return (
<div className={contentStyle.contentWrapper}> <div className={contentStyle.contentWrapper}>
<Spinner/> <Spinner/>
</div> </div>
); )
} }
return ( return (
<div> <div>
<Navbar /> <Navbar />
<div className={contentStyle.contentWrapper}> <div className={contentStyle.contentWrapper}>
<Header header={this.props.post.title} /> <Header header={title} />
<div className={contentStyle.content}> <div className={contentStyle.content}>
<div className={styles.postDate}> <div className={styles.postDate}>
<h3>{this.props.post.published}</h3> <h3>{date.format('MMMM D, YYYY')}</h3>
</div> </div>
<div className={styles.postContent} dangerouslySetInnerHTML={{__html: this.props.post.body}}> <div className={styles.postContent} dangerouslySetInnerHTML={{ __html: body }}>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import styles from './Spinner.scss'; import styles from './Spinner.scss'
export default class Spinner extends Component { export default class Spinner extends Component {
render() { render () {
return ( return (
<div className={styles.spinnerWrapper}> <div className={styles.spinnerWrapper}>
<div className={styles.ldsEllipsis}> <div className={styles.ldsEllipsis}>

View File

@ -1,9 +1,15 @@
import React, {Component} from 'react'; import PropTypes from 'prop-types'
import {Spinner, Header} from '.'; import React, { Component } from 'react'
import '../static/stylesheets/globals.scss'; import '../static/stylesheets/globals.scss'
import styles from './Wrapper.scss'; import styles from './Wrapper.scss'
export default class Wrapper extends Component { export default class Wrapper extends Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
}
render () { render () {
return ( return (

View File

@ -1,11 +1,11 @@
export { default as Home } from './Home'; export { default as Home } from './Home'
export { default as Blog } from './Blog'; export { default as Blog } from './Blog'
export { default as About } from './About'; export { default as About } from './About'
export { default as Post } from './Post'; export { default as Post } from './Post'
export { default as NotFoundPage } from './NotFoundPage'; export { default as NotFoundPage } from './NotFoundPage'
export { default as NotFoundWrapper } from './NotFoundWrapper'; export { default as NotFoundWrapper } from './NotFoundWrapper'
export { default as Spinner } from './Spinner'; export { default as Spinner } from './Spinner'
export { default as Header } from './Header'; export { default as Header } from './Header'
export { default as Wrapper } from './Wrapper'; export { default as Wrapper } from './Wrapper'
export { default as Navbar } from './Navbar'; export { default as Navbar } from './Navbar'
export { default as App } from './App'; export { default as App } from './App'

View File

@ -1,35 +1,33 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import axios from 'axios'; import PropTypes from 'prop-types'
import {About, Blog, Home, Wrapper} from '../components'; import { About, Blog, Home, Wrapper } from '../components'
export default class BlogContainer extends Component { export default class MainContainer extends Component {
static propTypes = {
staticContext: PropTypes.object
}
constructor() { constructor (props) {
super(); super(props)
let data
// eslint-disable-next-line no-undef
if (__isBrowser__) {
data = window.__INITIAL_DATA__
delete window.__INITIAL_DATA__
} else {
data = props.staticContext.data
}
this.state = { this.state = {
isLoadingBlog: true, isLoadingBlog: !data.posts,
isLoadingAbout: true, isLoadingAbout: !data.other.about,
about: data.other.about,
posts: data.posts
} }
} }
componentDidMount() { render () {
axios.get('/api/about').then((res) => {
this.setState({
isLoadingAbout: false,
about: res.data,
});
})
axios.get('/api/blog').then((res) => {
this.setState({
isLoadingBlog: false,
posts: res.data,
});
})
}
render() {
return ( return (
<div> <div>
<Home/> <Home/>

View File

@ -1,38 +1,32 @@
import React, {Component} from 'react'; import React, { Component } from 'react'
import axios from 'axios'; import PropTypes from 'prop-types'
import {Post, Wrapper, NotFoundPage} from '../components'; import { Post, Wrapper, NotFoundPage } from '../components'
export default class PostContainer extends Component { export default class PostContainer extends Component {
constructor() { static propTypes = {
super(); staticContext: PropTypes.object.isRequired
}
constructor (props) {
super(props)
let post
// eslint-disable-next-line no-undef
if (__isBrowser__) {
post = window.__INITIAL_DATA__
delete window.__INITIAL_DATA__
} else {
post = props.staticContext.data
}
this.state = { this.state = {
isLoading: true, isLoading: !post,
error: false, error: false,
}; post: post
}
} }
componentDidMount() { render () {
const url = '/api/post/' + this.props.match.params.postname;
axios.get(url).then((res) => {
if (res.data.error) {
this.setState({
error: true,
});
}
else {
this.setState({
error: false,
isLoading: false,
post: res.data,
});
}
})
}
render() {
if (this.state.error) { if (this.state.error) {
return ( return (
<NotFoundPage /> <NotFoundPage />
@ -42,8 +36,8 @@ export default class PostContainer extends Component {
return ( return (
<Wrapper> <Wrapper>
<Post isLoading={this.state.isLoading} <Post isLoading={this.state.isLoading}
post={this.state.post} /> post={this.state.post} />
</Wrapper> </Wrapper>
); )
} }
} }

View File

@ -1,2 +1,2 @@
export { default as MainContainer } from './MainContainer'; export { default as MainContainer } from './MainContainer'
export { default as PostContainer } from './PostContainer'; export { default as PostContainer } from './PostContainer'

View File

@ -1,59 +1,26 @@
require('babel-register'); import express from 'express'
var path = require('path'); import expressStaticGzip from 'express-static-gzip'
import { serverRender } from './utils/serverRender'
import { Scanner } from './utils/scanner'
var app = new (require('express'))(); const port = process.env.PORT || 3000
var port = process.env.PORT || 3000; const app = express()
const sass = require('node-sass'); const scanner = new Scanner()
scanner.scan()
require('css-modules-require-hook')({ app.use('/static', expressStaticGzip('public/static'))
generateScopedName: '[name]__[local]___[hash:base64:5]',
extensions: ['.scss', '.css'],
preprocessCss: (data, filename) => sass.renderSync({
data,
file: filename,
}).css
});
var fs = require('fs'); app.get('/favicon.ico', (req, res) => {
var filename = './src/utils/data.json'; res.status(404).send('Not Found !!!')
var dataStub = {"posts": [], "other": []}; })
fs.writeFileSync(filename, JSON.stringify(dataStub));
app.get('*', serverRender)
// initalize webpack dev middleware if in development context app.listen(port, function (error) {
if (process.env.NODE_ENV === 'development') {
var webpack = require('webpack')
var config = require('../webpack.config')
var devMiddleware = require('webpack-dev-middleware')
var hotDevMiddleware = require('webpack-hot-middleware')
var compiler = webpack(config)
var devMiddlewareConfig = {
noInfo: true,
stats: {colors: true},
publicPath: config.output.publicPath
}
app.use(devMiddleware(compiler, devMiddlewareConfig))
app.use(hotDevMiddleware(compiler))
}
require('./utils/scanner')();
var api = require('./utils/api');
app.use("/api", api);
var staticFiles = require('./utils/staticFiles');
app.use("/static", staticFiles);
var serverRender = require('./utils/serverRender');
app.get("*", serverRender);
app.listen(port, function(error) {
if (error) { if (error) {
console.error(error); console.error(error)
} else { } else {
console.info("[Server] Listening on port %s", port); console.info('[Server] Listening on port %s', port)
} }
}) })

View File

@ -1,56 +1,29 @@
const data = require('./data.json'); import fs from 'fs'
const api = require('express').Router(); import jsonfile from 'jsonfile'
const fs = require('fs'); import path from 'path'
const path = require('path'); import config from '../../config.json'
const config = require('../../config.json');
api.get('/blog', (req, res) => { export function getData (reqPath = '') {
res.set('Cache-Control', 'no-cache'); if (reqPath === '') {
data.posts.sort((a,b) => { return readData(config.dataPath)
return new Date(b.published) - new Date(a.published); } else {
const fileName = path.join(process.cwd(), config.contentPath, reqPath + '.md')
return readFile(fileName, 'utf8')
}
};
function readFile (fileName, options) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, options, (err, data) => {
err ? reject(err) : resolve(data)
})
}) })
res.json(data.posts); }
});
api.get('/about', (req, res) => { function readData (dataPath) {
const renderPath = path.join(process.cwd(), '/renders', 'about.html'); return new Promise(function (resolve, reject) {
res.set('Cache-Control', 'max-age=86400'); jsonfile.readFile(dataPath, (err, data) => {
fs.readFile(renderPath, 'utf8', (err, data) => { err ? reject(err) : resolve(data)
if (err) { })
res.json({ })
error: 404 }
});
} else {
res.json({
body: data,
});
}
});
});
api.get('/post/:postname', (req, res) => {
res.set('Cache-Control', 'no-cache');
const postname = req.params.postname;
const post = data.posts.find((el) => {
return el.filename === postname
});
const renderPath = path.join(process.cwd(), '/renders', postname + '.html');
fs.readFile(renderPath, 'utf8', (err, data) => {
if (err) {
res.json({
error: 404
});
} else {
res.json({
published: post.published,
link: post.link,
title: post.title,
body: data,
});
}
});
});
module.exports = api;

View File

@ -1,137 +0,0 @@
const MarkdownIt = require('markdown-it');
const fs = require('fs');
const path = require('path');
const moment = require('moment');
const jsonfile = require('jsonfile');
const async = require('async');
const fm = require('front-matter');
const config = require('../../config.json');
/**
* Renders file using MarkdownIt
*/
function render(file) {
const md = new MarkdownIt({html: true});
return md.render(file);
}
/**
* Extracts file metadata such as parent directory
*/
function fileMetadata(filepath) {
const paths = filepath.split('/');
const basename = path.basename(filepath);
const metadata = {
basename,
filename: basename.substr(0, basename.lastIndexOf('.')),
parrent: paths[paths.length - 2],
dirname: path.dirname(filepath),
};
return metadata;
}
/**
* Compiles file that is a blog post
*/
function compilePost(filepath, data, fileData, callback) {
const frontMatter = fm(fileData);
const rendered = render(frontMatter.body);
const metadata = fileMetadata(filepath);
if (frontMatter.attributes.draft) {
callback(null, null);
return;
}
let published;
if (frontMatter.attributes.date) {
published = moment(frontMatter.attributes.date);
} else {
published = moment();
}
const post = {
published: published.format('MMMM DD, YYYY'),
filename: metadata.filename,
title: frontMatter.attributes.title,
link: '/post/' + metadata.filename
};
const renderedpath = path.join(process.cwd(), config.renderPath, `${metadata.filename}.html`);
fs.writeFile(renderedpath, rendered, (err) => {
if (err) callback(err);
else callback(null, post);
});
}
/**
* Compiles other types of files such as resumes, about me and so on.
*/
function compileOther(filepath, data, fileData, callback) {
const frontMatter = fm(fileData);
const rendered = render(frontMatter.body);
const metadata = fileMetadata(filepath);
const post = {
filename: metadata.filename
}
const renderedpath = path.join(process.cwd(), config.renderPath, `${metadata.filename}.html`);
fs.writeFile(renderedpath, rendered, (err) => {
if (err) callback(err);
else callback(null, post);
});
}
function Compiler(data) {
this.data = data;
}
/**
*
*/
Compiler.prototype.addFile = function(filepath, isPost, callback) {
if (isPost) {
async.waterfall([
fs.readFile.bind(fs, filepath, 'utf8'),
compilePost.bind(compilePost, filepath, this.data),
], (err, result) => {
if (err) throw err;
if (result == null) {
callback();
} else {
this.data.posts.push(result);
console.log("[Compiler] File %s compiled", filepath);
callback();
}
});
} else {
async.waterfall([
fs.readFile.bind(fs, filepath, 'utf8'),
compileOther.bind(compileOther, filepath, this.data),
], (err, result) => {
if (err) throw err;
this.data.other.push(result);
console.log("[Compiler] File %s compiled", filepath);
callback();
});
}
};
/**
* Writes updated data to the data file
*/
Compiler.prototype.writeData = function(callback) {
const dataPath = path.join(process.cwd(), 'src/utils/data.json');
jsonfile.writeFile(dataPath, this.data, callback);
};
module.exports = Compiler;

22
src/utils/routes.js Normal file
View File

@ -0,0 +1,22 @@
import { MainContainer, PostContainer } from '../containers'
import { getData } from './api'
const routes = [
{
path: '/',
exact: true,
component: MainContainer,
getData: (path = '') => getData(
path.split('/').pop()
)
},
{
path: '/post/:postname',
component: PostContainer,
getData: (path = '') => getData(
path.split('/').pop()
)
}
]
export default routes

View File

@ -1,62 +1,127 @@
const fs = require('fs'); import fs from 'fs'
const path = require('path'); import path from 'path'
const async = require('async'); import config from '../../config.json'
const Compiler = require('./compiler'); import fm from 'front-matter'
const config = require('../../config.json'); import moment from 'moment'
const data = require('./data.json'); import jsonfile from 'jsonfile'
module.exports = function() { export class Scanner {
constructor () {
var compiler = new Compiler(data); this.data = {}
/**
* Reads the directory and returns it's content
*/
function readdir(callback) {
fs.readdir(config.contentPath, callback);
} }
/** readdir (dirname) {
* Calls compile on each file in the directory return new Promise((resolve, reject) => {
*/ fs.readdir(dirname, function (err, filenames) {
function compile(files, callback) { if (err) {
console.log("[Scanner] Discovered files: " + files); reject(err)
async.each(files, compileFile, (err) => { } else {
if (err) throw err; resolve(filenames)
callback(); }
}); })
})
} }
/** readfile (filename) {
* Helper function which calls compile in the Compiler module const filePath = path.join(process.cwd(), config.contentPath, filename)
*/ return new Promise((resolve, reject) => {
function compileFile(file, callback) { fs.readFile(filePath, 'utf8', (err, data) => {
const filePath = path.join(process.cwd(), config.contentPath, file); if (err) {
reject(err)
} else {
resolve([filename, data])
}
})
})
}
// config.files contains list of file names which are not considered blog posts processFile (file, data) {
if (config.files.indexOf(file) == -1) { const filePath = path.join(process.cwd(), config.contentPath, file)
compiler.addFile(filePath, true, callback); const metadata = this.fileMetadata(filePath)
if (config.files.indexOf(file) === -1) {
const frontMatter = fm(data)
if (frontMatter.attributes.draft) {
return
}
let published
if (frontMatter.attributes.date) {
published = moment(frontMatter.attributes.date)
} else {
published = moment()
}
const post = {
published: published.format('MMMM DD, YYYY'),
filename: metadata.filename,
title: frontMatter.attributes.title,
link: '/post/' + metadata.filename
}
this.data.posts.push(post)
} else { } else {
compiler.addFile(filePath, false, callback); this.data.other[metadata.filename] = data
} }
} }
/** init () {
* Writes updated data into the data file return new Promise((resolve, reject) => {
*/ jsonfile.readFile(config.dataPath, (err, data) => {
function writeData(callback) { if (err) {
compiler.writeData(callback); reject(err)
} else {
this.data = data
resolve(data)
}
})
})
} }
/** writeData (callback) {
* Main function. Scans the directory for files and compiles them into html return new Promise((resolve, reject) => {
* using the Compiler module jsonfile.writeFile(config.dataPath, this.data, (err, data) => {
*/ if (err) {
async.waterfall([ reject(err)
readdir, } else {
compile, resolve(this.data)
writeData }
], (err) => { })
if(err) throw err; })
}); }
fileMetadata (filepath) {
const paths = filepath.split('/')
const basename = path.basename(filepath)
const metadata = {
basename,
filename: basename.substr(0, basename.lastIndexOf('.')),
parrent: paths[paths.length - 2],
dirname: path.dirname(filepath)
}
return metadata
}
scan () {
this.init()
.then(
() => this.readdir(config.contentPath)
).then(
(files) => { return Promise.all(files.map(this.readfile)) }
).then(
(files) => {
files.forEach(
(item) => { this.processFile(item[0], item[1]) }
)
return this.writeData()
}
).then(
console.log('[Scanner] Scan complete')
).catch(
(err) => console.log(err)
)
}
} }

View File

@ -1,44 +1,48 @@
//import 'babel-polyfill'
import React from 'react' import React from 'react'
import { renderToString } from 'react-dom/server' import { renderToString } from 'react-dom/server'
import { StaticRouter as Router } from 'react-router-dom' import { StaticRouter as Router, matchPath } from 'react-router-dom'
import { App } from '../components/App' import { App } from '../components'
import routes from './routes'
import serialize from 'serialize-javascript'
import manifest from '../../public/static/manifest.json' import manifest from '../../public/static/manifest.json'
function serverRender(req, res) { export function serverRender (req, res, next) {
let markup = ''; const activeRoute = routes.find((route) => matchPath(req.url, route)) || {}
let status = 200;
const context = {} const promise = activeRoute.getData
markup = renderToString( ? activeRoute.getData(req.path)
<Router location={req.url} context={context}> : Promise.resolve()
<App />
</Router>,
);
return res.status(status).send(renderFullPage(markup, manifest)); promise.then((data) => {
const markup = renderToString(
<Router location={req.url} context={{ data }}>
<App/>
</Router>
)
res.status(200).send(renderFullPage(markup, data))
}).catch(next)
} }
function renderFullPage(html, manifest) { function renderFullPage (html, data) {
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Matúš Námešný</title> <title>Matúš Námešný</title>
<!-- Google Fonts --> <!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&amp;subset=latin-ext" rel="stylesheet" rel="preload"> <link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&amp;subset=latin-ext" rel="stylesheet" rel="preload">
<!-- Font Awesome --> <!-- Font Awesome -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous"> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<!-- Stylesheet --> <!-- Stylesheet -->
${process.env.NODE_ENV === 'production' ? `<link href=${manifest['bundle.css']} rel="stylesheet">` : ''} <link href=${manifest['bundle.css']} rel="stylesheet" rel="preload">
<!-- Initial Data -->
<script>window.__INITIAL_DATA__ = ${serialize(data)}</script>
</head> </head>
<body> <body>
<div id="root">${process.env.NODE_ENV === 'production' ? html : `<div>${html}</div>`}</div> <div id="root">${html}</div>
<script src="${manifest['bundle.js']}" async></script> <script src=${manifest['bundle.js']} async></script>
</body> </body>
</html> </html>
` `
} }
module.exports = serverRender;

View File

@ -1,40 +0,0 @@
const staticFiles = require('express').Router();
const path = require('path');
import manifest from '../../public/static/manifest.json'
staticFiles.get('/*.js', (req, res) => {
const filename = req.url.split("/").pop();
if (req.acceptsEncodings('gzip')) {
res.set({
'Content-Encoding': 'gzip',
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=31536000'
});
res.sendFile(path.join(process.cwd(), '/public/', manifest[`${filename}.gz`]));
} else {
res.set('Cache-Control', 'max-age=31536000');
res.sendFile(path.join(process.cwd(), '/public/', manifest['bundle.js']));
}
});
staticFiles.get('/*.css', (req, res) => {
const filename = req.url.split("/").pop();
if (req.acceptsEncodings('gzip')) {
res.set({
'Content-Encoding': 'gzip',
'Content-Type': 'text/css',
'Cache-Control': 'max-age=31536000'
});
res.sendFile(path.join(process.cwd(), '/public/', manifest[`${filename}.gz`]));
} else {
res.set('Cache-Control', 'max-age=31536000');
res.sendFile(path.join(process.cwd(), '/public/', manifest['bundle.css']));
}
});
staticFiles.get('*.jpg', (req, res) => {
res.set('Cache-Control', 'max-age=31536000');
res.sendFile(path.join(process.cwd(), '/public/static', req.url))
});
module.exports = staticFiles;

View File

@ -1,18 +1,17 @@
const { resolve, join } = require('path') const { resolve } = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const UglifyJsPlugin = require("uglifyjs-webpack-plugin") const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin") const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const CompressionPlugin = require("compression-webpack-plugin") const CompressionPlugin = require('compression-webpack-plugin')
const ManifestPlugin = require('webpack-manifest-plugin') const ManifestPlugin = require('webpack-manifest-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin') const CleanWebpackPlugin = require('clean-webpack-plugin')
const config = { const browserConfig = {
mode: 'production', mode: 'production',
context: resolve(__dirname, 'src'),
entry: { entry: {
bundle: [ bundle: [
'./app-client.js' './src/app-client.js'
] ]
}, },
output: { output: {
@ -42,7 +41,7 @@ const config = {
} }
}, },
{ {
loader: "postcss-loader" loader: 'postcss-loader'
}, },
{ {
loader: 'sass-loader' loader: 'sass-loader'
@ -56,25 +55,23 @@ const config = {
options: { options: {
limit: 8192 limit: 8192
} }
}, }
] ]
}, },
optimization: { optimization: {
minimizer: [ minimizer: [
new UglifyJsPlugin({ new UglifyJsPlugin(),
cache: true,
parallel: true,
sourceMap: true // set to true if you want JS source maps
}),
new OptimizeCSSAssetsPlugin({}) new OptimizeCSSAssetsPlugin({})
] ]
}, },
plugins: [ plugins: [
new CleanWebpackPlugin(['dist', 'public/static'], {}),
new MiniCssExtractPlugin({filename: '[name].[contenthash].css'}),
new CompressionPlugin({}),
new ManifestPlugin(), new ManifestPlugin(),
] new webpack.DefinePlugin({ __isBrowser__: 'true' }),
new CleanWebpackPlugin(['public/static', 'build'], {}),
new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
new CompressionPlugin({})
],
node: { fs: 'empty' }
} }
module.exports = config module.exports = browserConfig

View File

@ -1,74 +0,0 @@
const { resolve, join } = require('path')
const webpack = require('webpack')
const ManifestPlugin = require('webpack-manifest-plugin');
const config = {
mode: 'development',
devtool: 'cheap-eval-source-map',
context: resolve(__dirname, 'src'),
entry: {
bundle: [
'webpack-hot-middleware/client',
'./app-client.js'
]
},
output: {
path: resolve(__dirname,'public/static'),
filename: 'bundle.js',
publicPath: '/static/'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
],
exclude: '/node_modules/'
},
{
test: /\.scss$/,
use: [
{
loader: "style-loader"
},
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 2,
localIdentName: '[name]__[local]___[hash:base64:5]'
}
},
{
loader: "postcss-loader"
},
{
loader: "sass-loader"
}
]
},
{
test: /\.(png|jpg)$/,
exclude: /node_modules/,
loader: 'url-loader'
},
{
test: /\.(html)$/,
use: {
loader: 'html-loader',
options: {
attrs: [':data-src']
}
}
},
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.NamedModulesPlugin(),
new ManifestPlugin({'writeToFileEmit': true}),
]
}
module.exports = config

72
webpack.server.js Normal file
View File

@ -0,0 +1,72 @@
const { resolve } = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CreateFileWebpack = require('create-file-webpack')
const serverConfig = {
entry: './src/server.js',
target: 'node',
externals: [nodeExternals()],
output: {
path: resolve(__dirname, 'build'),
filename: 'server.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
],
exclude: '/node_modules/'
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: true,
exportOnlyLocals: true,
importLoaders: 2,
localIdentName: '[name]__[local]___[hash:base64:5]'
}
},
{
loader: 'postcss-loader'
},
{
loader: 'sass-loader'
}
]
},
{
test: /\.(png|jpg)$/,
exclude: /node_modules/,
loader: 'url-loader',
options: {
limit: 10000
}
}
]
},
plugins: [
new webpack.DefinePlugin({
__isBrowser__: 'false'
}),
new MiniCssExtractPlugin(),
new CreateFileWebpack({
path: './src/utils/',
fileName: 'data.json',
content: JSON.stringify({
'posts': [],
'other': {}
})
})
]
}
module.exports = serverConfig

2216
yarn.lock

File diff suppressed because it is too large Load Diff