",
6 | "scripts": {
7 | "dev": "nodemon ./server/babel-register.js",
8 | "start": "node ./server/babel-register.js",
9 | "precommit": "npm run lint",
10 | "lint": "eslint --ext .html,.js,.vue --ignore-path .gitignore .",
11 | "docker-publish": "docker build -t eggplanet/gustavo . && docker push eggplanet/gustavo"
12 | },
13 | "dependencies": {
14 | "@nuxtjs/component-cache": "^1.1.0",
15 | "@nuxtjs/google-analytics": "^1.0.0",
16 | "@nuxtjs/pwa": "^1.0.2",
17 | "apicache": "^1.1.0",
18 | "axios": "^0.16.2",
19 | "babel-polyfill": "^6.26.0",
20 | "babel-register": "^6.26.0",
21 | "compression": "^1.7.1",
22 | "express": "^4.16.1",
23 | "express-http-proxy": "^1.0.7",
24 | "highlight.js": "^9.12.0",
25 | "html-element": "^2.2.0",
26 | "lodash.uniqby": "^4.7.0",
27 | "morgan": "^1.9.0",
28 | "node-sass": "^4.5.3",
29 | "normalize.css": "^7.0.0",
30 | "nuxt": "^1.0.0-rc11",
31 | "sass-loader": "^6.0.6",
32 | "showdown": "^1.7.6",
33 | "source-map-support": "^0.5.0",
34 | "vue-moment": "^2.0.2",
35 | "xmldom": "^0.1.27"
36 | },
37 | "babel-cli": "^6.26.0",
38 | "devDependencies": {
39 | "babel-eslint": "^8.0.1",
40 | "babel-preset-env": "^1.6.0",
41 | "eslint": "^4.8.0",
42 | "eslint-config-standard": "^10.2.1",
43 | "eslint-plugin-html": "^3.2.2",
44 | "eslint-plugin-import": "^2.7.0",
45 | "eslint-plugin-node": "^5.2.0",
46 | "eslint-plugin-promise": "^3.5.0",
47 | "eslint-plugin-standard": "^3.0.1",
48 | "eslint-plugin-vue": "^2.1.0",
49 | "nodemon": "^1.12.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pages/_id.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
51 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
26 |
27 |
32 |
--------------------------------------------------------------------------------
/pages/post/_id.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
47 |
--------------------------------------------------------------------------------
/plugins/to-iso-date.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | Vue.use({
3 | install () {
4 | Vue.filter('toIsoDate', dateString => new Date(dateString))
5 | }
6 | })
7 |
--------------------------------------------------------------------------------
/plugins/vue-moment.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueMoment from 'vue-moment'
3 | Vue.use(VueMoment)
4 |
--------------------------------------------------------------------------------
/server/api/content.js:
--------------------------------------------------------------------------------
1 | import proxy from 'express-http-proxy'
2 | import config from '../../nuxt.config.js'
3 |
4 | const githubToken = config.gustavo.githubToken || process.env.GITHUB_TOKEN
5 | const gistId = config.gustavo.gistId || process.env.GIST_ID
6 |
7 | /* eslint-disable no-console */
8 | if (typeof githubToken === 'undefined') {
9 | console.warn(`Github Token not provided. You will be rate limited.`)
10 | }
11 |
12 | if (!gistId) {
13 | throw new Error(`No Gist ID found in config or via ENV variable.`)
14 | }
15 |
16 | const url = `api.github.com`
17 |
18 | const defaults = {
19 | https: true,
20 | proxyReqPathResolver: (req) => {
21 | return `/gists/${gistId}`
22 | },
23 | proxyReqOptDecorator (proxyReqOpts, srcReq) {
24 | proxyReqOpts.headers['Authorization'] = `token ${githubToken}`
25 | return proxyReqOpts
26 | }
27 | }
28 |
29 | export const post = proxy(url, Object.assign({}, defaults, {
30 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
31 | let files = JSON.parse(proxyResData.toString('utf8')).files
32 | const fileNames = Object.keys(files)
33 | const fileName = fileNames.find(name => name === `${userReq.params.id}.post.md`)
34 |
35 | if (!fileName) {
36 | userRes.status(404)
37 | return 'Not found.'
38 | } else {
39 | return JSON.stringify({ post: files[fileName] })
40 | }
41 | }
42 | }))
43 |
44 | export const page = proxy(url, Object.assign({}, defaults, {
45 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
46 | let files = JSON.parse(proxyResData.toString('utf8')).files
47 | const fileNames = Object.keys(files)
48 | const fileName = fileNames.find(name => name === `${userReq.params.id}.page.md`)
49 |
50 | if (!fileName) {
51 | userRes.status(404)
52 | return 'Not found.'
53 | } else {
54 | return JSON.stringify({ page: files[fileName] })
55 | }
56 | }
57 | }))
58 |
59 | export const posts = proxy(url, Object.assign({}, defaults, {
60 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
61 | let files = JSON.parse(proxyResData.toString('utf8')).files
62 | const fileNames = Object.keys(files)
63 | const posts = []
64 |
65 | fileNames
66 | .filter(name => !name.endsWith('.draft.post.md'))
67 | .forEach(name => {
68 | if (name.endsWith('.post.md')) {
69 | posts.push(files[name])
70 | }
71 | })
72 |
73 | return JSON.stringify({ posts })
74 | }
75 | }))
76 |
77 | export const links = proxy(url, Object.assign({}, defaults, {
78 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
79 | let files = JSON.parse(proxyResData.toString('utf8')).files
80 | const links = files[`links.md`]
81 | return JSON.stringify({ links })
82 | }
83 | }))
84 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { links, page, post, posts } from './content'
3 | import apicache from 'apicache'
4 |
5 | const router = express.Router()
6 |
7 | const gustavoConfig = require(process.cwd() + '/gustavo.config')
8 | const cacheDuration = gustavoConfig.cacheDuration || '60 minutes'
9 |
10 | if (process.env.NODE_ENV === 'development') {
11 | router.use(apicache.middleware(cacheDuration))
12 | }
13 |
14 | router.use('/posts/:id', post)
15 | router.use('/posts', posts)
16 | router.use('/pages/:id', page)
17 | router.use('/links', links)
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/server/babel-register.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 | module.exports = require('./index.js').default
3 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import nuxt from 'nuxt'
2 | import express from 'express'
3 | import api from './api'
4 | import morgan from 'morgan'
5 | import config from '../nuxt.config.js'
6 |
7 | // Shouldn't throw.
8 | require(process.cwd() + '/gustavo.config')
9 |
10 | const {
11 | Nuxt,
12 | Builder
13 | } = nuxt
14 |
15 | config.dev = !(process.env.NODE_ENV === 'production')
16 |
17 | const app = express()
18 | const host = process.env.HOST || '0.0.0.0'
19 | const port = process.env.PORT || 3000
20 |
21 | app.use(morgan('tiny'))
22 |
23 | app.use('/favicon.ico', (req, res) => res.end())
24 | app.set('port', port)
25 | app.use('/api', api)
26 |
27 | const n = new Nuxt(config)
28 | new Builder(n).build()
29 |
30 | function start () {
31 | app.use(n.render)
32 | app.listen(port, host)
33 | console.log(`Server listening on ${host}:${port}`) // eslint-disable-line no-console
34 | }
35 |
36 | start()
37 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eggplanetio/gustavo/cf0590fd54216ba7bf970db495bea5df1fe638a6/static/favicon.png
--------------------------------------------------------------------------------
/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eggplanetio/gustavo/cf0590fd54216ba7bf970db495bea5df1fe638a6/static/icon.png
--------------------------------------------------------------------------------
/store/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import parser from './parser'
3 |
4 | export function state () {
5 | return {
6 | currentPage: {},
7 | currentPost: {},
8 | posts: [],
9 | links: [],
10 | host: '',
11 | navHidden: true
12 | }
13 | }
14 |
15 | export const getters = {
16 | contentUrl: state => {
17 | let host
18 | if (typeof window === 'undefined') {
19 | host = `http://0.0.0.0:${process.env.runningPort}`
20 | } else {
21 | host = `//${window.location.host}`
22 | }
23 | return `${host}/api`
24 | }
25 | }
26 |
27 | export const actions = {
28 | async FETCH_PAGE ({ dispatch, commit }, id) {
29 | let { data: { page } } = await axios.get(`${getters.contentUrl()}/pages/${id}`)
30 | commit('SET_CURRENT_PAGE', page)
31 | },
32 | async FETCH_POST ({ commit }, id) {
33 | let { data: { post } } = await axios.get(`${getters.contentUrl()}/posts/${id}`)
34 | commit('SET_CURRENT_POST', post)
35 | },
36 | async FETCH_POSTS ({ commit }) {
37 | let { data: { posts } } = await axios.get(`${getters.contentUrl()}/posts`)
38 | commit('SET_POSTS', posts)
39 | }
40 | }
41 |
42 | export const mutations = {
43 | SET_CURRENT_POST (state, post) {
44 | state.currentPost = parser.parsePost(post)
45 | },
46 | SET_CURRENT_PAGE (state, page) {
47 | state.currentPage = parser.parsePage(page)
48 | },
49 | SET_POSTS (state, posts) {
50 | state.posts = parser.parsePosts(posts)
51 | },
52 | TOGGLE_NAV (state) {
53 | state.navHidden = !state.navHidden
54 | },
55 | HIDE_NAV (state) {
56 | state.navHidden = true
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/store/parser.js:
--------------------------------------------------------------------------------
1 | import showdown from 'showdown'
2 | const converter = new showdown.Converter({
3 | tables: true
4 | })
5 |
6 | let Parser
7 | if (typeof DOMParser === 'undefined') {
8 | Parser = require('xmldom').DOMParser
9 | } else {
10 | Parser = DOMParser
11 | }
12 |
13 | const makeDOM = (html) => {
14 | return new Parser().parseFromString(html, 'text/html')
15 | }
16 |
17 | export default {
18 | parseItem (raw, split) {
19 | let content = converter.makeHtml(raw.content)
20 | const doc = makeDOM(content)
21 |
22 | const meta = {}
23 | const metaNodes = Array.from(doc.getElementsByTagName('meta'))
24 | metaNodes.forEach(node => {
25 | meta[node.getAttribute('name')] = node.getAttribute('content')
26 | })
27 |
28 | const segment = raw.filename.split(split)[0]
29 | const path = `/post/${segment}`
30 | const id = raw.filename.split(split)[0]
31 | content = content.replace(//g, '')
32 |
33 | const firstSentence = content.slice(3)
34 | .split('.')[0] + '.'
35 |
36 | return {
37 | id,
38 | content,
39 | path,
40 | meta,
41 | firstSentence
42 | }
43 | },
44 |
45 | parsePost (rawHtml) {
46 | return this.parseItem(rawHtml, '.post.md')
47 | },
48 |
49 | parsePage (rawHtml) {
50 | return this.parseItem(rawHtml, '.page.md')
51 | },
52 |
53 | parsePosts (files) {
54 | return files
55 | .filter(file => file.filename.includes('.post.md'))
56 | .map(raw => this.parseItem(raw, '.post'))
57 | .sort((current, other) => new Date(other.meta.date) - new Date(current.meta.date))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------