Merge branch 'netlify-cms'

This commit is contained in:
Jinksi 2017-11-18 12:46:38 +10:00
commit c3ce1dd09e
27 changed files with 2153 additions and 1339 deletions

2
.gitignore vendored
View file

@ -13,3 +13,5 @@ build
.DS_Store
.env
npm-debug.log
src/data.json

30
content/pages/about.md Normal file
View file

@ -0,0 +1,30 @@
---
title: About page title
subtitle: <About />
featuredImage: /images/uploads/isabella-juskova-260426.jpg
section1: |-
## Hello World!
Netlify CMS works with both `.md` and `.json`.
![Image test!](/images/uploads/unsplash4.jpg)
section2: >-
## This is a Container component
A sem vel nec sodales mi vivamus senectus sed potenti a parturient nascetur
tincidunt nisi pulvinar rhoncus a. Risus imperdiet taciti suspendisse facilisi
a per metus cubilia varius a nostra adipiscing amet ultrices quisque ac mi a.
Dictumst a ultrices mi a dignissim ad fermentum eget a nam et a blandit
scelerisque. Taciti lorem tempor quam vestibulum dis habitasse vestibulum diam
vel est ut proin dis auctor. Suscipit scelerisque orci magna interdum vel
bibendum duis netus a consectetur dui magnis ac aliquet sem posuere tincidunt
vestibulum.
The image below will have a `srcset` attribute generated:
![Tim Marshall](/images/uploads/tim-marshall-155597.jpg)
---

View file

@ -0,0 +1 @@
{"title":"Contact","subtitle":"<Contact />","body":"# Example contact form\n\nThis form is setup to use Netlify's form handling:\n\n- the form action is set to the current absolute url: `action: '/contact/'`\n- a name attribute is sent with the form's data `'form-name': 'Contact'`\n- netlify data attributes are added to the form `data-netlify data-netlify-honeypot`\n\nFind out more in the [Netlify Docs](https://www.netlify.com/docs/form-handling/).\n"}

1
content/pages/home.json Normal file
View file

@ -0,0 +1 @@
{"title":"Home","subtitle":"<Home />","body":"# 🍉 HyperStatic\n\nA not-so-static site boilerplate:\n\n* **Create React App** for simplicity\n* **Styled Components** for component-based css\n* **React Router** for routing (v4)\n* **React Helmet** for document titles, descriptions, meta\n* **React Snapshot** for pre-rendering to static html so it works without Javascript ⭐️\n* [**Netlify CMS**](https://netlifycms.org)"}

View file

@ -0,0 +1 @@
siteTitle: HyperStatic

View file

@ -0,0 +1,3 @@
{
"siteTitle": "HyperStatic"
}

View file

@ -0,0 +1,89 @@
const fs = require('fs')
const path = require('path')
const _set = require('lodash/set')
const _mergeWith = require('lodash/mergeWith')
const _isArray = require('lodash/isArray')
const globCb = require('glob')
const util = require('util')
const glob = util.promisify(globCb)
const readFile = util.promisify(fs.readFile)
const matter = require('gray-matter')
const yaml = require('js-yaml')
const options = {
contentDir: './content/',
outputFile: './src/data.json'
}
const getCollectionType = filePath => {
const pathParsed = path.parse(filePath)
const objectKey = pathParsed.dir
.replace(options.contentDir, '')
.replace(/\//g, '.')
return `${objectKey}`
}
const getDocumentName = filePath => {
const pathParsed = path.parse(filePath)
return `${pathParsed.name}`
}
const getDocumentExt = filePath => {
const pathParsed = path.parse(filePath)
return `${pathParsed.ext}`
}
const parseMarkdown = data => {
data = matter(data)
data = { ...data, ...data.data }
delete data.data
return JSON.stringify(data)
}
const parseYaml = data => {
try {
data = yaml.safeLoad(data, 'utf8')
return JSON.stringify(data)
} catch (e) {
return console.log(e)
}
}
const getFileContents = filePath => {
return readFile(filePath, 'utf8').then(data => {
if (getDocumentExt(filePath) === '.md') {
data = parseMarkdown(data)
}
if (['.yaml', '.yml'].includes(getDocumentExt(filePath))) {
data = parseYaml(data)
}
let documentData = JSON.parse(data)
documentData.name = getDocumentName(filePath)
let obj = {}
_set(obj, getCollectionType(filePath), [documentData])
console.log(`✨ Processed ${filePath}`)
return obj
})
}
const readFiles = async paths => Promise.all(paths.map(getFileContents))
const combineJSON = async () => {
// mergeCustomiser concats arrays items
const mergeCustomiser = (objValue, srcValue) =>
_isArray(objValue) ? objValue.concat(srcValue) : objValue
console.log(`✨ Reading JSON files in ${options.contentDir}`)
const paths = await glob(`${options.contentDir}/**/**.+(json|md|yaml|yml)`)
const results = await readFiles(paths)
const data = _mergeWith({}, ...results, mergeCustomiser)
return JSON.stringify(data, null, 2)
}
const writeJSON = async () => {
const json = await combineJSON()
fs.writeFileSync(options.outputFile, json)
console.log(`✅ Data saved to ${options.outputFile}`)
}
writeJSON()

View file

@ -0,0 +1,83 @@
const fs = require('fs')
const path = require('path')
const globCb = require('glob')
const util = require('util')
const sharp = require('sharp')
const glob = util.promisify(globCb)
const readFile = util.promisify(fs.readFile)
const options = {
inputDir: './public/images/uploads',
outputDir: './public/images/uploads/resized',
sizes: [300, 600, 1200, 1800],
imageFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp']
}
const saveImage = ({ buffer, size, outputFile }) => {
return new Promise((resolve, reject) => {
sharp(buffer)
.resize(size)
.toFile(outputFile, err => {
if (err) {
return reject(err)
} else {
return resolve(console.log(`✅ Saved ${outputFile}`))
}
})
})
}
const saveImages = ({ buffer, filename }) => {
console.log(`🎞 Processing ${filename}`)
return Promise.all(
options.sizes.map(async size => {
const extname = path.extname(filename)
const newFilename = `${path.basename(
filename,
extname
)}.${size}${extname}`
const outputFile = `${options.outputDir}/${newFilename}`
const fileExists = await doesFileExist({ filename: outputFile })
if (fileExists) return console.log(`↩️ ${outputFile} exists, skipping`)
return saveImage({ buffer, size, outputFile })
})
)
}
const readFiles = files =>
Promise.all(
files.map(async filename => {
const buffer = await readFile(filename)
return { filename, buffer }
})
)
const doesFileExist = async ({ filename }) => {
try {
await readFile(filename)
return true
} catch (e) {
return false
}
}
const resizeImages = async () => {
console.log(`✨ Reading image files in ${options.inputDir}`)
try {
const fileGlob = `${options.inputDir}/**/**.+(${options.imageFormats.join(
'|'
)})`
const files = await glob(fileGlob)
const ignore = new RegExp(
`(${options.sizes.join('|')}).(${options.imageFormats.join('|')})$`
)
const filesToResize = files.filter(filename => !filename.match(ignore))
const imageFiles = await readFiles(filesToResize)
imageFiles.map(saveImages)
} catch (e) {
console.log(e)
}
}
resizeImages()

View file

@ -12,13 +12,18 @@
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^7.4.0",
"eslint-plugin-standard": "^3.0.1",
"glob": "^7.1.2",
"gray-matter": "^3.1.1",
"js-yaml": "^3.10.0",
"react-scripts": "^1.0.10",
"sharp": "^0.18.4",
"snazzy": "^7.0.0",
"standard": "^10.0.2",
"sw-precache": "^5.2.0"
},
"dependencies": {
"polished": "1.7.0",
"netlify-identity-widget": "^1.2.0",
"polished": "^1.7.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-helmet": "^5.1.3",
@ -29,15 +34,17 @@
"styled-components": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build && react-snapshot && npm run sw",
"start":
"npm run parse-content && npm run resize-images && react-scripts start",
"build":
"npm run parse-content && npm run resize-images && react-scripts build && react-snapshot && npm run sw",
"parse-content": "node ./functions/parse-content.js",
"resize-images": "node ./functions/resize-images.js",
"sw": "sw-precache --config='sw-precache-config.js'",
"test": "standard | snazzy && react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"reactSnapshot": {
"include": [
"/404"
]
"include": ["/404"]
}
}

52
public/admin/config.yml Normal file
View file

@ -0,0 +1,52 @@
# See https://github.com/netlify/netlify-cms/blob/master/example/config.yml
backend:
name: git-gateway
branch: netlify-cms # Branch to update (optional; defaults to master)
media_folder: "public/images/uploads" # Media files will be stored in the repo under static/images/uploads
public_folder: "/images/uploads" # The src attribute for uploaded media will begin with /images/uploads
collections: # A list of collections the CMS should be able to edit
- name: "settings"
label: "Settings"
delete: false # Prevent users from deleting documents in this collection
editor:
preview: false
files:
- file: "content/settings/global.json"
label: "Global Settings"
name: "global-settings"
fields:
- {label: Site Title, name: siteTitle, widget: string}
- file: "content/settings/global-yaml.yml"
label: "Global Settings - Yaml"
name: "global-settings-yaml"
fields:
- {label: Site Title, name: siteTitle, widget: string}
- name: "pages"
label: "Pages"
files:
- file: "content/pages/home.json"
label: "Home Page"
name: "home-page"
fields:
- {label: Title, name: title, widget: string}
- {label: Subtitle, name: subtitle, widget: string}
- {label: Body, name: body, widget: markdown}
# markdown files are ok
- file: "content/pages/about.md"
label: "About Page"
name: "about-page"
fields:
- {label: Title, name: title, widget: string}
- {label: Subtitle, name: subtitle, widget: string}
- {label: Featured Image, name: featuredImage, widget: image}
- {label: Section 1, name: section1, widget: markdown}
- {label: Section 2, name: section2, widget: markdown}
- file: "content/pages/contact.json"
label: "Contact Page"
name: "contact-page"
fields:
- {label: Title, name: title, widget: string}
- {label: Subtitle, name: subtitle, widget: string}
- {label: Body, name: body, widget: markdown}

24
public/admin/index.html Normal file
View file

@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
<!-- Include the styles for the Netlify CMS UI, after your own styles -->
<link rel="stylesheet" href="https://unpkg.com/netlify-cms/dist/cms.css" />
</head>
<body>
<!-- Include the script that builds the page and powers Netlify CMS -->
<script src="https://identity-js.netlify.com/v1/netlify-identity-widget.js"></script>
<script src="https://unpkg.com/netlify-cms@0.7.3/dist/cms.js"></script>
<script>
// redirect when logging out
window.netlifyIdentity.on('logout', function () {
document.location.href = '/'
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View file

@ -8,63 +8,46 @@ import About from './views/About'
import Contact from './views/Contact'
import NoMatch from './views/NoMatch'
import Nav from './components/Nav'
import NavLink from './components/NavLink'
import Logo from './components/Logo'
import GithubCorner from './components/GithubCorner'
import ServiceWorkerNotifications from './components/ServiceWorkerNotifications'
import globalStyles from './globalStyles'
export const siteTitle = 'HyperStatic'
const routes = [
{
title: 'Home',
path: '/',
comp: Home,
exact: true
}, {
title: 'About',
path: '/about/',
comp: About,
exact: true
}, {
title: 'Contact',
path: '/contact/',
comp: Contact,
exact: true
}
]
import data from './data.json'
class App extends Component {
componentWillMount () {
globalStyles()
state = {
data
}
componentWillMount () {
globalStyles()
import('./netlifyIdentity')
}
getDocument = (collection, name) =>
this.state.data[collection] && this.state.data[collection].filter(page => page.name === name)[0]
getDocuments = (collection) => this.state.data[collection]
render () {
const site = this.getDocument('settings', 'global')
return (
<Router>
<div>
<ScrollToTop />
<ServiceWorkerNotifications
readyMessage='This message is displayed when the SW is registered'
/>
<ServiceWorkerNotifications />
<GithubCorner url='https://github.com/Jinksi/hyperstatic' />
<Helmet titleTemplate={`${siteTitle} | %s`} />
<Nav>
<Logo>
<span role='img' aria-label='Watermelon'>🍉</span>
</Logo>
{routes.map((route, i) => (
<NavLink key={i} {...route} />
))}
</Nav>
<Helmet titleTemplate={`${site.siteTitle} | %s`} />
<Nav />
<Switch>
{routes.map((route, i) => (
<Route
{...route}
key={i}
render={() => <route.comp {...route} />}
/>
))}
<Route path='/' exact
render={(props) => <Home page={this.getDocument('pages', 'home')} {...props} />}
/>
<Route path='/about/' exact
render={(props) => <About page={this.getDocument('pages', 'about')} {...props} />}
/>
<Route path='/contact/' exact
render={(props) => <Contact page={this.getDocument('pages', 'contact')} site={site} {...props} />}
/>
<Route component={NoMatch} />
</Switch>
</div>

26
src/components/Content.js Normal file
View file

@ -0,0 +1,26 @@
import React from 'react'
import styled from 'styled-components'
import Marked from 'react-markdown'
import { getImageSrc, getImageSrcset } from '../util/getImageUrl'
export default ({ source }) => (
<Marked
source={source}
renderers={{
Image: ImageWithSrcset
}}
/>
)
const Image = styled.img`
width: 100%;
height: auto;
`
const ImageWithSrcset = props => (
<Image
{...props}
src={getImageSrc(props.src)}
srcSet={getImageSrcset(props.src)}
/>
)

View file

@ -1,6 +1,8 @@
import React from 'react'
import styled from 'styled-components'
import { Container, Flex } from './common'
import NavLink from './NavLink'
import Logo from './Logo'
const Nav = styled.div`
background: white;
@ -11,7 +13,14 @@ const Nav = styled.div`
export default (props) => (
<Nav>
<Container>
<Flex alignCenter {...props} />
<Flex alignCenter>
<Logo>
<span role='img' aria-label='Watermelon'>🍉</span>
</Logo>
<NavLink to='/' exact>Home</NavLink>
<NavLink to='/about/' exact>About</NavLink>
<NavLink to='/contact/' exact>Contact</NavLink>
</Flex>
</Container>
</Nav>
)

View file

@ -20,10 +20,10 @@ const NavLink = styled.span`
}
`
export default ({ path, exact, ...props }) => (
<Route path={path} exact={exact} children={({match}) => (
export default ({ to, exact, match, children }) => (
<Route path={to} exact={exact} children={({match}) => (
<NavLink active={match}>
<Link to={path}>{props.title}</Link>
<Link to={to}>{children}</Link>
</NavLink>
)} />
)

View file

@ -2,7 +2,6 @@ import React, { Component } from 'react'
import styled from 'styled-components'
import { stringify } from 'qs'
import { color } from '../globalStyles'
import { siteTitle } from '../App'
const fetch = window.fetch
class Form extends Component {
@ -10,7 +9,7 @@ class Form extends Component {
name: '',
email: '',
message: '',
subject: `New Submission from ${siteTitle}!`,
subject: `New Submission from ${this.props.siteTitle}!`,
_gotcha: '',
disabled: false,
alert: '',
@ -60,7 +59,7 @@ class Form extends Component {
name: '',
email: '',
message: '',
subject: `New Submission from ${siteTitle}!`,
subject: `New Submission from ${this.props.siteTitle}!`,
_gotcha: ''
})
})

View file

@ -1,8 +1,9 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Section, Container } from './common'
import { Section, Container, BackgroundImage } from './common'
import { getImageSrc } from '../util/getImageUrl'
import { color } from '../globalStyles'
const Header = styled(Section)`
@ -10,14 +11,21 @@ const Header = styled(Section)`
color: ${color.primary};
background: ${color.primary};
color: white;
h2{
h2 {
font-weight: 200;
}
`
const PageHeader = ({ title, subtitle }) => (
<Header>
<Container>
const PageHeader = ({ title, subtitle, backgroundImage }) => (
<Header relative>
{backgroundImage && (
<BackgroundImage
image={getImageSrc(backgroundImage, '1200')}
opacity={0.5}
/>
)}
<Container relative>
<h1>{title}</h1>
{subtitle ? <h2>{subtitle}</h2> : ''}
</Container>

14
src/netlifyIdentity.js Normal file
View file

@ -0,0 +1,14 @@
import netlifyIdentity from 'netlify-identity-widget'
// check for netlifyIdentity, redirect to admin if user is logging in
netlifyIdentity.on('init', user => {
if (!user) {
netlifyIdentity.on('login', () => {
document.location.href = '/admin/'
})
}
})
if (window.localStorage) {
netlifyIdentity.init()
}

43
src/util/getImageUrl.js Normal file
View file

@ -0,0 +1,43 @@
const sizes = [300, 600, 1200, 1800]
const outputDir = '/images/uploads/'
const resizedDir = '/images/uploads/resized/'
const getImageSrcset = path => {
if (path.indexOf('http') >= 0) {
return console.warn('Cannot get srcset for external image: ' + path)
}
const filename = path.split('.').shift()
const extname = path.split('.').pop()
const pathname = encodeURI(filename.replace(outputDir, resizedDir))
const srcset = sizes
.map(size => `${pathname}.${size}.${extname} ${size}w`)
.join(', ')
return srcset
}
const getImageSrc = (path, sizeRequested) => {
if (path.indexOf('http') >= 0) return path
sizeRequested = parseInt(sizeRequested, 10)
let size
if (sizeRequested) {
// rounds up to nearest size or returns largest
size =
sizes.filter(num => num >= sizeRequested)[0] || sizes[sizes.length - 1]
} else {
// get the middle size
size = sizes[Math.ceil(sizes.length / 2)]
}
const extname = path.split('.').pop()
const filename = path.split('.').shift()
const pathname = encodeURI(filename.replace(outputDir, resizedDir))
return `${pathname}.${size}.${extname}`
}
module.exports = {
getImageSrcset,
getImageSrc,
sizes,
outputDir
}

View file

@ -1,26 +1,30 @@
import React from 'react'
import Helmet from 'react-helmet'
import Page from '../components/Page'
import PageHeader from '../components/PageHeader'
import { Container, Section } from '../components/common'
import Content from '../components/Content.js'
export default ({ title }) => (
export default ({ page }) => (
<Page>
<PageHeader title={title} subtitle='<About />' />
<Helmet>
<title>{page.title}</title>
</Helmet>
<PageHeader
title={page.title}
subtitle={page.subtitle}
backgroundImage={page.featuredImage}
/>
<Section thin>
<Container>
<h1>Hello World!</h1>
<p>A sem vel nec sodales mi vivamus senectus sed potenti a parturient nascetur tincidunt nisi pulvinar rhoncus a. Risus imperdiet taciti suspendisse facilisi a per metus cubilia varius a nostra adipiscing amet ultrices quisque ac mi a. Dictumst a ultrices mi a dignissim ad fermentum eget a nam et a blandit scelerisque. Taciti lorem tempor quam vestibulum dis habitasse vestibulum diam vel est ut proin dis auctor. Suscipit scelerisque orci magna interdum vel bibendum duis netus a consectetur dui magnis ac aliquet sem posuere tincidunt vestibulum.</p>
<Content source={page.section1} />
</Container>
</Section>
<Section thin>
<Container taCenter skinny>
<h1>This is a skinny center-aligned {'<Container />'}</h1>
<p>A sem vel nec sodales mi vivamus senectus sed potenti a parturient nascetur tincidunt nisi pulvinar rhoncus a. Risus imperdiet taciti suspendisse facilisi a per metus cubilia varius a nostra adipiscing amet ultrices quisque ac mi a. Dictumst a ultrices mi a dignissim ad fermentum eget a nam et a blandit scelerisque. Taciti lorem tempor quam vestibulum dis habitasse vestibulum diam vel est ut proin dis auctor. Suscipit scelerisque orci magna interdum vel bibendum duis netus a consectetur dui magnis ac aliquet sem posuere tincidunt vestibulum.</p>
<Container>
<Content source={page.section2} />
</Container>
</Section>
<Helmet>
<title>{title}</title>
</Helmet>
</Page>
)

View file

@ -7,24 +7,12 @@ import NetlifySimpleForm from '../components/NetlifySimpleForm'
import { Container, Section } from '../components/common'
import Marked from 'react-markdown'
const content = `
# Example contact form
This form is setup to use Netlify's form handling:
- the form action is set to the current absolute url: \`action: '/contact/'\`
- a name attribute is sent with the form's data \`'form-name': 'Contact'\`
- netlify data attributes are added to the form \`data-netlify data-netlify-honeypot\`
Find out more in the [Netlify Docs](https://www.netlify.com/docs/form-handling/).
`
export default ({ title }) => (
export default ({ page, site }) => (
<Page>
<PageHeader title={title} subtitle='<Contact />' />
<PageHeader title={page.title} subtitle='<Contact />' />
<Section thin>
<Container>
<Marked source={content} />
<Marked source={page.body} />
<br />
<h3>{'<NetlifyControlledForm />'}</h3>
<NetlifyControlledForm />
@ -37,7 +25,7 @@ export default ({ title }) => (
</Container>
</Section>
<Helmet>
<title>{title}</title>
<title>{page.title}</title>
</Helmet>
</Page>
)

View file

@ -2,29 +2,17 @@ import React from 'react'
import Helmet from 'react-helmet'
import Page from '../components/Page'
import { Container, Section } from '../components/common'
import Content from '../components/Content'
import PageHeader from '../components/PageHeader'
import Marked from 'react-markdown'
const content = `
# 🍉 HyperStatic
A not-so-static site boilerplate:
- **Create React App** for simplicity
- **Styled Components** for component-based css
- **React Router** for routing (v4)
- **React Helmet** for document titles, descriptions, meta
- **React Snapshot** for pre-rendering to static html so it works without Javascript
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Jinksi/hyperstatic)
`
export default ({ title }) => {
export default ({ page }) => {
const { title, subtitle } = page
return (
<Page>
<PageHeader title={title} subtitle='<Home />' />
<PageHeader title={title} subtitle={subtitle} />
<Section thin>
<Container>
<Marked source={content} />
<Content source={page.body} />
</Container>
</Section>
<Helmet>

2927
yarn.lock

File diff suppressed because it is too large Load diff